jishushell 0.5.15 → 0.5.22

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 (97) hide show
  1. package/Dockerfile.hermes-slim +2 -5
  2. package/apps/filebrowser-container.yaml +1 -0
  3. package/apps/ollama-binary.yaml +44 -0
  4. package/apps/ollama-with-hollama-binary.yaml +45 -1
  5. package/dist/cli/doctor.js +144 -16
  6. package/dist/cli/doctor.js.map +1 -1
  7. package/dist/install.js +1 -1
  8. package/dist/install.js.map +1 -1
  9. package/dist/routes/instances.js +42 -5
  10. package/dist/routes/instances.js.map +1 -1
  11. package/dist/routes/llm.js +29 -0
  12. package/dist/routes/llm.js.map +1 -1
  13. package/dist/server.js +18 -4
  14. package/dist/server.js.map +1 -1
  15. package/dist/services/agent-apps/catalog.d.ts +3 -0
  16. package/dist/services/agent-apps/catalog.js +40 -13
  17. package/dist/services/agent-apps/catalog.js.map +1 -1
  18. package/dist/services/agent-apps/installers/shell-script.d.ts +1 -1
  19. package/dist/services/agent-apps/installers/shell-script.js +19 -2
  20. package/dist/services/agent-apps/installers/shell-script.js.map +1 -1
  21. package/dist/services/agent-apps/types.d.ts +3 -0
  22. package/dist/services/app/app-manager.d.ts +8 -0
  23. package/dist/services/app/app-manager.js +77 -3
  24. package/dist/services/app/app-manager.js.map +1 -1
  25. package/dist/services/app/openclaw-manager.js +17 -2
  26. package/dist/services/app/openclaw-manager.js.map +1 -1
  27. package/dist/services/backup-manager.js +43 -4
  28. package/dist/services/backup-manager.js.map +1 -1
  29. package/dist/services/capability-endpoint-validator.js +26 -7
  30. package/dist/services/capability-endpoint-validator.js.map +1 -1
  31. package/dist/services/instance-manager.js +89 -9
  32. package/dist/services/instance-manager.js.map +1 -1
  33. package/dist/services/llm-proxy/index.d.ts +28 -0
  34. package/dist/services/llm-proxy/index.js +76 -3
  35. package/dist/services/llm-proxy/index.js.map +1 -1
  36. package/dist/services/llm-proxy/validate-key.d.ts +41 -0
  37. package/dist/services/llm-proxy/validate-key.js +672 -0
  38. package/dist/services/llm-proxy/validate-key.js.map +1 -0
  39. package/dist/services/macos-launchd.d.ts +89 -0
  40. package/dist/services/macos-launchd.js +273 -0
  41. package/dist/services/macos-launchd.js.map +1 -0
  42. package/dist/services/nomad-manager.d.ts +7 -0
  43. package/dist/services/nomad-manager.js +290 -79
  44. package/dist/services/nomad-manager.js.map +1 -1
  45. package/dist/services/panel-manager.js +20 -10
  46. package/dist/services/panel-manager.js.map +1 -1
  47. package/dist/services/runtime/adapters/custom.js +56 -0
  48. package/dist/services/runtime/adapters/custom.js.map +1 -1
  49. package/dist/services/runtime/adapters/hermes.d.ts +4 -3
  50. package/dist/services/runtime/adapters/hermes.js +165 -63
  51. package/dist/services/runtime/adapters/hermes.js.map +1 -1
  52. package/dist/services/runtime/adapters/openclaw.d.ts +28 -0
  53. package/dist/services/runtime/adapters/openclaw.js +502 -4
  54. package/dist/services/runtime/adapters/openclaw.js.map +1 -1
  55. package/dist/services/setup-manager.js +97 -50
  56. package/dist/services/setup-manager.js.map +1 -1
  57. package/dist/services/update-manager.js +32 -14
  58. package/dist/services/update-manager.js.map +1 -1
  59. package/dist/types.d.ts +1 -0
  60. package/install/jishu-install.sh +247 -35
  61. package/install/jishu-uninstall.sh +45 -5
  62. package/package.json +5 -2
  63. package/public/assets/ApiKeyField-CvyAOcJS.js +1 -0
  64. package/public/assets/Dashboard-AuJESBlJ.js +1 -0
  65. package/public/assets/{HermesChatPanel-B_2HlVBQ.js → HermesChatPanel-CByPREwb.js} +1 -1
  66. package/public/assets/HermesConfigForm-DRda8FKX.js +4 -0
  67. package/public/assets/InitPassword-ka4wNpM5.js +1 -0
  68. package/public/assets/InstanceDetail-Cg1nS8HX.js +92 -0
  69. package/public/assets/Login-aPajuQzf.js +1 -0
  70. package/public/assets/NewInstance-Dd1ebNIx.js +1 -0
  71. package/public/assets/ProviderRecommendations-DFmADQ7V.js +1 -0
  72. package/public/assets/Settings-BYQnbLYL.js +1 -0
  73. package/public/assets/Setup-D05lwDOV.js +1 -0
  74. package/public/assets/WeixinLoginPanel-D89kdhP4.js +9 -0
  75. package/public/assets/index-HSXCsceK.css +1 -0
  76. package/public/assets/{index-BZc5zH7u.js → index-bnBu0nlQ.js} +7 -7
  77. package/public/assets/registry-C_qeFTkZ.js +2 -0
  78. package/public/assets/usePolling-Bn93fe7M.js +1 -0
  79. package/public/assets/{vendor-i18n-y9V7Sfuu.js → vendor-i18n-flxcMVeP.js} +2 -2
  80. package/public/assets/{vendor-react-BWrEVJVb.js → vendor-react-ZC5T_huj.js} +1 -1
  81. package/public/index.html +4 -4
  82. package/scripts/check-colima-launchd.mjs +230 -0
  83. package/public/assets/Dashboard-BdWPtroF.js +0 -1
  84. package/public/assets/HermesConfigForm-DVlhg3WV.js +0 -4
  85. package/public/assets/InitPassword-D7glTExX.js +0 -1
  86. package/public/assets/InstanceDetail-CxSy2cpe.js +0 -92
  87. package/public/assets/Login-Cfr5c2sv.js +0 -1
  88. package/public/assets/NewInstance-BIYDmJis.js +0 -1
  89. package/public/assets/ProviderRecommendations-BuRnvRcI.js +0 -1
  90. package/public/assets/Settings-Cc-tYBil.js +0 -1
  91. package/public/assets/Setup-lGZEk5jq.js +0 -1
  92. package/public/assets/WeixinLoginPanel-CoGqzxeV.js +0 -9
  93. package/public/assets/index-87IJXG-w.css +0 -1
  94. package/public/assets/input-paste-CrNVAyOy.js +0 -1
  95. package/public/assets/providers-DtNXh9JD.js +0 -1
  96. package/public/assets/registry-BWnkJgZ1.js +0 -2
  97. package/public/assets/usePolling-CwwT9KrC.js +0 -1
@@ -35,7 +35,7 @@
35
35
  */
36
36
  import { execFile, execFileSync } from "child_process";
37
37
  import { accessSync, chmodSync, chownSync, copyFileSync, cpSync, constants, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync, } from "fs";
38
- import { randomBytes } from "crypto";
38
+ import { createHash, randomBytes } from "crypto";
39
39
  import { homedir, userInfo } from "os";
40
40
  import { delimiter, dirname, join, resolve as pathResolve } from "path";
41
41
  import { getNomadDriver, getOpenclawDockerImage, JISHUSHELL_HOME, getPanelConfig, } from "../../../config.js";
@@ -48,7 +48,7 @@ import { getInstanceDir as framework_instanceDir, instanceMetaPath } from "../..
48
48
  import { createTask, emitTask, spawnWithTask, getDirSizeMB, npmProgressParser, dockerBuildProgressParser, resolveDockerInvocation, } from "../../setup-manager.js";
49
49
  import { DEFAULT_OPENCLAW_DOCKER_IMAGE, setOpenclawDockerImage, OPENCLAW_MODULES, OPENCLAW_PKG_DIR, } from "../../../config.js";
50
50
  import { fileURLToPath } from "node:url";
51
- import { bootstrapInstanceProxy } from "../../llm-proxy/index.js";
51
+ import { bootstrapInstanceProxy, decryptApiKey, getInstanceConfig as getProxyInstanceConfig } from "../../llm-proxy/index.js";
52
52
  import { registerAdapter } from "../registry.js";
53
53
  import { registerOpenclawRoutes } from "./openclaw-routes.js";
54
54
  // ── Constants physically migrated from nomad-manager.ts ───────────────
@@ -340,6 +340,65 @@ function patchJsproxyBaseUrl(configPath) {
340
340
  console.warn(`[openclaw] Failed to patch jsproxy baseUrl in ${configPath}: ${e.message}`);
341
341
  }
342
342
  }
343
+ /** Known private/internal hostnames used by JishuShell's local proxy. */
344
+ const PRIVATE_NETWORK_HOSTS = ["host.docker.internal", "127.0.0.1", "localhost", "0.0.0.0"];
345
+ export function isPrivateNetworkBaseUrl(baseUrl) {
346
+ try {
347
+ const url = new URL(baseUrl);
348
+ if (PRIVATE_NETWORK_HOSTS.includes(url.hostname))
349
+ return true;
350
+ if (url.hostname.startsWith("10."))
351
+ return true;
352
+ if (url.hostname.startsWith("192.168."))
353
+ return true;
354
+ // RFC 1918: 172.16.0.0/12 → 172.16.x.x through 172.31.x.x
355
+ const m = url.hostname.match(/^172\.(\d+)\./);
356
+ if (m) {
357
+ const second = parseInt(m[1], 10);
358
+ if (second >= 16 && second <= 31)
359
+ return true;
360
+ }
361
+ return false;
362
+ }
363
+ catch {
364
+ return false;
365
+ }
366
+ }
367
+ /**
368
+ * Ensure `request.allowPrivateNetwork: true` is set on providers whose baseUrl
369
+ * targets a private/internal host (e.g. host.docker.internal). Without this,
370
+ * OpenClaw's SSRF guard blocks requests to the JishuShell local proxy.
371
+ */
372
+ export function patchPrivateNetworkAllowFlag(configPath) {
373
+ try {
374
+ const raw = readFileSync(configPath, "utf-8");
375
+ const config = JSON.parse(raw);
376
+ const providers = config?.models?.providers;
377
+ if (!providers || typeof providers !== "object")
378
+ return;
379
+ let changed = false;
380
+ for (const [, provider] of Object.entries(providers)) {
381
+ if (typeof provider !== "object" || provider === null)
382
+ continue;
383
+ const p = provider;
384
+ if (typeof p.baseUrl !== "string")
385
+ continue;
386
+ if (!isPrivateNetworkBaseUrl(p.baseUrl))
387
+ continue;
388
+ if (p.request?.allowPrivateNetwork === true)
389
+ continue;
390
+ p.request = { ...(p.request || {}), allowPrivateNetwork: true };
391
+ changed = true;
392
+ }
393
+ if (changed) {
394
+ writeConfigFile(configPath, JSON.stringify(config, null, 2));
395
+ console.log(`[openclaw] Patched request.allowPrivateNetwork in ${configPath} for private-network providers`);
396
+ }
397
+ }
398
+ catch (e) {
399
+ console.warn(`[openclaw] Failed to patch allowPrivateNetwork in ${configPath}: ${e.message}`);
400
+ }
401
+ }
343
402
  /**
344
403
  * Docker bridge port publishing cannot reach a process that only binds the
345
404
  * container loopback. Normalize default/loopback gateway binds to `lan` so
@@ -969,7 +1028,368 @@ function ensureOpenclawUpdateSeed(openclawHome, instanceId) {
969
1028
  console.warn(`[openclaw] update-seed ${instanceId}: failed to create seed: ${err?.message ?? err}`);
970
1029
  }
971
1030
  }
972
- // ── Resource helpers (migrated from nomad-manager.ts) ─────────────────
1031
+ // ── Session fence patch (OpenClaw >= 5.19 regression) ─────────────────
1032
+ /**
1033
+ * The minimum OpenClaw version where the session fence bug exists.
1034
+ * The bug was introduced in v2026.5.19 — `eventMayReachTranscriptWriters()`
1035
+ * doesn't recognize `"message"` / `"custom_message"` event types, causing
1036
+ * session writes to bypass `withSessionWriteLock`, which in turn leaves
1037
+ * the file fingerprint stale and triggers `EmbeddedAttemptSessionTakeoverError`
1038
+ * on the next prompt.
1039
+ *
1040
+ * Additionally, on virtiofs mounts (macOS → Colima VM → Docker), file
1041
+ * metadata (mtime/ctime) can drift between `releaseForPrompt()` and
1042
+ * `assertSessionFileFence()` even without actual writes, due to the
1043
+ * multi-layer filesystem stack timing. Patching `assertSessionFileFence`
1044
+ * to a no-op fully disables the flawed fence check.
1045
+ */
1046
+ const SESSION_FENCE_BUG_MIN_VERSION = [2026, 5, 19];
1047
+ /**
1048
+ * Maximum version (exclusive) to apply the patch. Once OpenClaw ships a fix
1049
+ * upstream, set a concrete version here to stop patching newer releases.
1050
+ * For now, no upper bound — patch all versions >= MIN.
1051
+ */
1052
+ /**
1053
+ * Safety limit: if the extracted function body exceeds this many characters,
1054
+ * skip the patch to avoid corrupting the bundle (e.g. brace-depth counter
1055
+ * fooled by string literals containing braces in a future refactored version).
1056
+ */
1057
+ const MAX_PATCH_TARGET_BODY_LENGTH = 5000;
1058
+ /**
1059
+ * Patch targets: each entry defines a function to replace.
1060
+ * - `eventMayReachTranscriptWriters`: broad return prevents events from
1061
+ * bypassing the write lock (necessary but not sufficient on virtiofs).
1062
+ * - `assertSessionFileFence`: no-op disables the fingerprint comparison
1063
+ * that false-triggers due to virtiofs stat drift.
1064
+ */
1065
+ const SESSION_FENCE_PATCH_TARGETS = [
1066
+ {
1067
+ fnName: "function assertSessionFileFence()",
1068
+ litNeedle: "sameSessionFileFingerprint",
1069
+ replacement: "function assertSessionFileFence() { return; }",
1070
+ },
1071
+ {
1072
+ fnName: "function eventMayReachTranscriptWriters",
1073
+ litNeedle: '"message_update"',
1074
+ replacement: 'function eventMayReachTranscriptWriters(session, event) { return typeof event?.type === "string"; }',
1075
+ },
1076
+ ];
1077
+ /**
1078
+ * Patch the session fence bug in OpenClaw >= 5.19 by replacing the
1079
+ * `eventMayReachTranscriptWriters` function in the minified bundle.
1080
+ *
1081
+ * Idempotent: uses a marker file containing the SHA-256 of the patched
1082
+ * file. Skips if already patched or if the pattern is not found (future
1083
+ * fixed version or different bundle layout).
1084
+ *
1085
+ * Docker-only: the bug manifests through the virtiofs mount timing
1086
+ * discrepancy between container and host — raw_exec/process modes are
1087
+ * unaffected in practice.
1088
+ */
1089
+ export function patchSessionFenceBug(openclawHome, instanceId) {
1090
+ if (getNomadDriver() !== "docker")
1091
+ return;
1092
+ // 1. Read installed version from npm-global package.json
1093
+ const pkgPath = join(openclawHome, ".npm-global", "lib", "node_modules", "openclaw", "package.json");
1094
+ let versionStr;
1095
+ try {
1096
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
1097
+ versionStr = pkg.version || "";
1098
+ }
1099
+ catch {
1100
+ return; // no npm-global install or unreadable — skip
1101
+ }
1102
+ // 2. Parse version and check minimum threshold
1103
+ const parts = versionStr.split(".").map(Number);
1104
+ if (parts.length < 3 || parts.some(Number.isNaN))
1105
+ return;
1106
+ const [major, minor, patch] = parts;
1107
+ const [minMajor, minMinor, minPatch] = SESSION_FENCE_BUG_MIN_VERSION;
1108
+ if (major < minMajor ||
1109
+ (major === minMajor && minor < minMinor) ||
1110
+ (major === minMajor && minor === minMinor && patch < minPatch)) {
1111
+ return; // version predates the bug
1112
+ }
1113
+ // 3. Find the selection-*.js bundle that contains the target function.
1114
+ // There may be multiple selection-*.js files in dist/; only one holds
1115
+ // the session management code.
1116
+ const distDir = join(openclawHome, ".npm-global", "lib", "node_modules", "openclaw", "dist");
1117
+ let bundleFiles;
1118
+ try {
1119
+ const entries = readdirSync(distDir);
1120
+ bundleFiles = entries.filter((f) => f.startsWith("selection-") && f.endsWith(".js"));
1121
+ }
1122
+ catch {
1123
+ return; // dist dir missing or unreadable
1124
+ }
1125
+ if (bundleFiles.length === 0)
1126
+ return;
1127
+ // Scan each candidate for any of the patch target signatures
1128
+ let bundlePath;
1129
+ let bundleContent;
1130
+ for (const file of bundleFiles) {
1131
+ const candidatePath = join(distDir, file);
1132
+ let content;
1133
+ try {
1134
+ content = readFileSync(candidatePath, "utf-8");
1135
+ }
1136
+ catch {
1137
+ continue;
1138
+ }
1139
+ // Match if any target's fnName or litNeedle is present
1140
+ const hasTarget = SESSION_FENCE_PATCH_TARGETS.some((t) => content.includes(t.fnName) || content.includes(t.litNeedle));
1141
+ if (hasTarget) {
1142
+ bundlePath = candidatePath;
1143
+ bundleContent = content;
1144
+ break;
1145
+ }
1146
+ }
1147
+ if (!bundlePath || !bundleContent)
1148
+ return;
1149
+ // 4. Check marker file for idempotency
1150
+ const markerPath = join(distDir, `.session-fence-patched`);
1151
+ const currentHash = createHash("sha256").update(bundleContent).digest("hex");
1152
+ try {
1153
+ const marker = readFileSync(markerPath, "utf-8").trim();
1154
+ if (marker === currentHash)
1155
+ return; // already patched, hash matches
1156
+ }
1157
+ catch {
1158
+ // marker missing — proceed
1159
+ }
1160
+ // 5. Apply each patch target using brace-depth counting to extract and
1161
+ // replace function bodies. Process targets in order; earlier patches
1162
+ // shift offsets so we re-search after each replacement.
1163
+ let patched = bundleContent;
1164
+ let patchCount = 0;
1165
+ for (const target of SESSION_FENCE_PATCH_TARGETS) {
1166
+ let fnStart = patched.indexOf(target.fnName);
1167
+ // Fallback for eventMayReachTranscriptWriters: locate by unique literal
1168
+ // combination ("message_update" near "message_end" and "agent_end")
1169
+ if (fnStart === -1 && target.litNeedle === '"message_update"') {
1170
+ let scanPos = 0;
1171
+ while (scanPos < patched.length) {
1172
+ const litIdx = patched.indexOf(target.litNeedle, scanPos);
1173
+ if (litIdx === -1)
1174
+ break;
1175
+ const window = patched.slice(litIdx, litIdx + 200);
1176
+ if (window.includes('"message_end"') && window.includes('"agent_end"')) {
1177
+ const searchStart = Math.max(0, litIdx - 300);
1178
+ const prefix = patched.slice(searchStart, litIdx);
1179
+ const fnKeywordIdx = prefix.lastIndexOf("function ");
1180
+ if (fnKeywordIdx !== -1) {
1181
+ fnStart = searchStart + fnKeywordIdx;
1182
+ break;
1183
+ }
1184
+ }
1185
+ scanPos = litIdx + 1;
1186
+ }
1187
+ }
1188
+ if (fnStart === -1)
1189
+ continue; // target not found (already patched or fixed version)
1190
+ // Check if already patched (replacement is short and contains `return;`)
1191
+ const peekEnd = Math.min(fnStart + target.replacement.length + 10, patched.length);
1192
+ const peek = patched.slice(fnStart, peekEnd);
1193
+ if (peek.startsWith(target.replacement))
1194
+ continue;
1195
+ // Extract function body with brace-depth counting
1196
+ const braceStart = patched.indexOf("{", fnStart);
1197
+ if (braceStart === -1)
1198
+ continue;
1199
+ let depth = 0;
1200
+ let fnEnd = -1;
1201
+ for (let i = braceStart; i < patched.length; i++) {
1202
+ if (patched[i] === "{")
1203
+ depth++;
1204
+ else if (patched[i] === "}") {
1205
+ depth--;
1206
+ if (depth === 0) {
1207
+ fnEnd = i + 1;
1208
+ break;
1209
+ }
1210
+ }
1211
+ }
1212
+ if (fnEnd === -1)
1213
+ continue;
1214
+ // Safety: if extracted body is suspiciously large, skip to avoid corrupting
1215
+ // the bundle (brace counter may have been fooled by string contents).
1216
+ if (fnEnd - fnStart > MAX_PATCH_TARGET_BODY_LENGTH) {
1217
+ console.warn(`[openclaw] session-fence-patch ${instanceId}: skipping ${target.fnName} — body too large (${fnEnd - fnStart} chars)`);
1218
+ continue;
1219
+ }
1220
+ patched = patched.slice(0, fnStart) + target.replacement + patched.slice(fnEnd);
1221
+ patchCount++;
1222
+ }
1223
+ if (patchCount === 0) {
1224
+ // No targets needed patching — write marker for current state
1225
+ try {
1226
+ writeFileSync(markerPath, currentHash + "\n", "utf-8");
1227
+ }
1228
+ catch {
1229
+ /* best effort */
1230
+ }
1231
+ return;
1232
+ }
1233
+ // 7. Atomic write: temp file → rename
1234
+ const tmpPath = bundlePath + `.tmp.${randomBytes(4).toString("hex")}`;
1235
+ try {
1236
+ writeFileSync(tmpPath, patched, "utf-8");
1237
+ renameSync(tmpPath, bundlePath);
1238
+ }
1239
+ catch (err) {
1240
+ // Clean up temp file on failure
1241
+ try {
1242
+ unlinkSync(tmpPath);
1243
+ }
1244
+ catch {
1245
+ /* ignore */
1246
+ }
1247
+ console.warn(`[openclaw] session-fence-patch ${instanceId}: atomic write failed: ${err?.message ?? err}`);
1248
+ return;
1249
+ }
1250
+ // 8. Write marker with hash of the PATCHED file
1251
+ const patchedHash = createHash("sha256").update(patched).digest("hex");
1252
+ try {
1253
+ writeFileSync(markerPath, patchedHash + "\n", "utf-8");
1254
+ }
1255
+ catch {
1256
+ // Non-fatal — patch was applied, marker just helps idempotency
1257
+ }
1258
+ console.log(`[openclaw] session-fence-patch ${instanceId}: patched ${bundlePath.split("/").pop()} (v${versionStr})`);
1259
+ }
1260
+ // ── Container-side patch script ───────────────────────────────────────────
1261
+ /**
1262
+ * Self-contained Node.js script that patches the session fence bug from
1263
+ * INSIDE the container. Written to `$OPENCLAW_HOME/.jishushell/session-fence-patch.mjs`
1264
+ * by onBeforeStart and executed by the custom entrypoint wrapper before
1265
+ * the OpenClaw gateway starts.
1266
+ *
1267
+ * This covers the case where an in-container `npm install openclaw@latest`
1268
+ * upgrades to a buggy version (>= 5.19) after the container was created
1269
+ * with a clean image (5.7). The host-side `patchSessionFenceBug` cannot
1270
+ * patch at onBeforeStart time because the npm-global directory may not yet
1271
+ * contain the upgraded bundle (it's a dangling symlink to a container-only path).
1272
+ */
1273
+ const CONTAINER_PATCH_SCRIPT = `#!/usr/bin/env node
1274
+ // Session fence patch — auto-generated by JishuShell.
1275
+ // Patches assertSessionFileFence + eventMayReachTranscriptWriters
1276
+ // in OpenClaw >= 2026.5.19 to work around virtiofs stat drift.
1277
+ import { readFileSync, writeFileSync, readdirSync, renameSync, existsSync } from "fs";
1278
+ import { join } from "path";
1279
+ import { createHash } from "crypto";
1280
+
1281
+ const home = process.env.HOME || process.env.OPENCLAW_HOME || "/home/openclaw";
1282
+ const distDir = join(home, ".npm-global", "lib", "node_modules", "openclaw", "dist");
1283
+ const pkgPath = join(home, ".npm-global", "lib", "node_modules", "openclaw", "package.json");
1284
+
1285
+ try {
1286
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
1287
+ const parts = (pkg.version || "").split(".").map(Number);
1288
+ if (parts.length < 3 || parts[0] < 2026 || (parts[0] === 2026 && parts[1] < 5) ||
1289
+ (parts[0] === 2026 && parts[1] === 5 && parts[2] < 19)) process.exit(0);
1290
+ // No upper version cap for now — patch all versions >= 5.19
1291
+ } catch { process.exit(0); }
1292
+
1293
+ const markerPath = join(distDir, ".session-fence-patched");
1294
+ let files;
1295
+ try { files = readdirSync(distDir).filter(f => f.startsWith("selection-") && f.endsWith(".js")); }
1296
+ catch { process.exit(0); }
1297
+
1298
+ const targets = [
1299
+ { fn: "function assertSessionFileFence()", repl: "function assertSessionFileFence() { return; }" },
1300
+ { fn: "function eventMayReachTranscriptWriters", repl: 'function eventMayReachTranscriptWriters(session, event) { return typeof event?.type === "string"; }' },
1301
+ ];
1302
+
1303
+ for (const file of files) {
1304
+ const p = join(distDir, file);
1305
+ let src;
1306
+ try { src = readFileSync(p, "utf-8"); } catch { continue; }
1307
+ if (!targets.some(t => src.includes(t.fn) || src.includes("sameSessionFileFingerprint"))) continue;
1308
+
1309
+ const hash = createHash("sha256").update(src).digest("hex");
1310
+ try { if (readFileSync(markerPath, "utf-8").trim() === hash) process.exit(0); } catch {}
1311
+
1312
+ let modified = src;
1313
+ let count = 0;
1314
+ for (const t of targets) {
1315
+ const idx = modified.indexOf(t.fn);
1316
+ if (idx === -1) continue;
1317
+ if (modified.slice(idx, idx + t.repl.length + 5).startsWith(t.repl)) continue;
1318
+ const braceStart = modified.indexOf("{", idx);
1319
+ if (braceStart === -1) continue;
1320
+ let d = 0, end = -1;
1321
+ for (let i = braceStart; i < modified.length; i++) {
1322
+ if (modified[i] === "{") d++;
1323
+ else if (modified[i] === "}") { d--; if (d === 0) { end = i + 1; break; } }
1324
+ }
1325
+ if (end === -1) continue;
1326
+ if (end - idx > 5000) continue; // safety: skip if body suspiciously large
1327
+ modified = modified.slice(0, idx) + t.repl + modified.slice(end);
1328
+ count++;
1329
+ }
1330
+ if (count === 0) {
1331
+ try { writeFileSync(markerPath, hash + "\\n"); } catch {}
1332
+ process.exit(0);
1333
+ }
1334
+ const tmp = p + ".tmp." + Math.random().toString(36).slice(2, 8);
1335
+ writeFileSync(tmp, modified);
1336
+ renameSync(tmp, p);
1337
+ const newHash = createHash("sha256").update(modified).digest("hex");
1338
+ try { writeFileSync(markerPath, newHash + "\\n"); } catch {}
1339
+ console.log("[session-fence-patch] patched " + file);
1340
+ break;
1341
+ }
1342
+ `;
1343
+ /**
1344
+ * The custom entrypoint wrapper that executes the patch script before
1345
+ * deferring to the image's original entrypoint.
1346
+ */
1347
+ const CONTAINER_ENTRYPOINT_WRAPPER = `#!/bin/sh
1348
+ # JishuShell session-fence-patch entrypoint wrapper
1349
+ PATCH="$HOME/.jishushell/session-fence-patch.mjs"
1350
+ if [ -f "$PATCH" ]; then
1351
+ node "$PATCH" 2>/dev/null || true
1352
+ fi
1353
+ exec /usr/local/bin/openclaw-entry.sh "$@"
1354
+ `;
1355
+ /**
1356
+ * Write the container-side patch script and entrypoint wrapper to the
1357
+ * openclaw-home directory so they are available inside the container
1358
+ * via the bind mount.
1359
+ *
1360
+ * Called from onBeforeStart. Idempotent — only writes if content changed.
1361
+ */
1362
+ export function writeContainerPatchScript(openclawHome) {
1363
+ const scriptDir = join(openclawHome, ".jishushell");
1364
+ try {
1365
+ mkdirSync(scriptDir, { recursive: true });
1366
+ }
1367
+ catch {
1368
+ return;
1369
+ }
1370
+ const scriptPath = join(scriptDir, "session-fence-patch.mjs");
1371
+ const wrapperPath = join(scriptDir, "entrypoint.sh");
1372
+ // Write patch script (idempotent)
1373
+ try {
1374
+ const existing = readFileSync(scriptPath, "utf-8");
1375
+ if (existing === CONTAINER_PATCH_SCRIPT) {
1376
+ // Also check wrapper
1377
+ const existingWrapper = readFileSync(wrapperPath, "utf-8");
1378
+ if (existingWrapper === CONTAINER_ENTRYPOINT_WRAPPER)
1379
+ return;
1380
+ }
1381
+ }
1382
+ catch {
1383
+ // file missing — write it
1384
+ }
1385
+ try {
1386
+ writeFileSync(scriptPath, CONTAINER_PATCH_SCRIPT, { mode: 0o755 });
1387
+ writeFileSync(wrapperPath, CONTAINER_ENTRYPOINT_WRAPPER, { mode: 0o755 });
1388
+ }
1389
+ catch {
1390
+ /* best effort */
1391
+ }
1392
+ }
973
1393
  function resolveUidGid(username) {
974
1394
  try {
975
1395
  if (!VALID_USER_RE.test(username)) {
@@ -1330,6 +1750,7 @@ class OpenClawAdapter {
1330
1750
  // 3. Docker bridge patches
1331
1751
  patchDockerBridgeGatewayBind(configPath);
1332
1752
  patchJsproxyBaseUrl(configPath);
1753
+ patchPrivateNetworkAllowFlag(configPath);
1333
1754
  }
1334
1755
  // Driver-agnostic: enable the OpenAI-compatible endpoints on every
1335
1756
  // start so the `llm-agent` capability advertised by openclaw-*.yaml
@@ -1345,6 +1766,22 @@ class OpenClawAdapter {
1345
1766
  catch {
1346
1767
  /* best effort */
1347
1768
  }
1769
+ // 4a. Patch session fence bug in OpenClaw >= 5.19
1770
+ try {
1771
+ const home = openclawAdapter.resolveAgentHome(instanceId);
1772
+ if (home) {
1773
+ patchSessionFenceBug(home, instanceId);
1774
+ // Also write the container-side patch script so that even if
1775
+ // the host-side patch couldn't apply (e.g. symlink to container
1776
+ // path), the container will self-patch on startup.
1777
+ if (getNomadDriver() === "docker") {
1778
+ writeContainerPatchScript(home);
1779
+ }
1780
+ }
1781
+ }
1782
+ catch {
1783
+ /* best effort — patch failure must not prevent start */
1784
+ }
1348
1785
  // 4b. Build the workspace symlink layout from this instance's
1349
1786
  // fileMounts (M1 W2). For docker mode, the corresponding
1350
1787
  // volume bindings are added in buildNomadTask below; for
@@ -2199,6 +2636,8 @@ class OpenClawAdapter {
2199
2636
  : [...DEFAULT_ARGS];
2200
2637
  const env = { ...DEFAULT_ENV };
2201
2638
  Object.assign(env, im.getRuntimeEnv(instanceId));
2639
+ decryptRuntimeProviderEnv(env);
2640
+ injectProviderHostEnv(env, instanceId);
2202
2641
  delete env.JSPROXY_API_KEY; // supplied via Nomad Template from Variables
2203
2642
  env.OPENCLAW_HOME = openclawHome;
2204
2643
  env.OPENCLAW_INSTANCE_ID = instanceId;
@@ -2257,6 +2696,11 @@ class OpenClawAdapter {
2257
2696
  // timeout is too short for Pi-class networks pulling a 1+ GiB
2258
2697
  // openclaw runtime image; bump to 15 minutes.
2259
2698
  image_pull_timeout: "15m",
2699
+ // Use the JishuShell entrypoint wrapper that applies the session
2700
+ // fence patch before deferring to the image's original entrypoint.
2701
+ // The wrapper script is written to $HOME/.jishushell/entrypoint.sh
2702
+ // by onBeforeStart (writeContainerPatchScript).
2703
+ entrypoint: [`${openclawHome}/.jishushell/entrypoint.sh`],
2260
2704
  args,
2261
2705
  work_dir: openclawHome,
2262
2706
  volumes: buildVolumes(openclawHome, im.getInstanceRuntime(instanceId)),
@@ -3006,6 +3450,10 @@ function prepareConfigForSave(instanceId, config) {
3006
3450
  if (typeof p.api === "string" && p.api in LEGACY_PROVIDER_API_ALIASES) {
3007
3451
  p.api = LEGACY_PROVIDER_API_ALIASES[p.api];
3008
3452
  }
3453
+ // Ensure allowPrivateNetwork for providers targeting the local proxy
3454
+ if (typeof p.baseUrl === "string" && isPrivateNetworkBaseUrl(p.baseUrl)) {
3455
+ p.request = { ...(p.request || {}), allowPrivateNetwork: true };
3456
+ }
3009
3457
  if (!("apiKey" in p))
3010
3458
  continue;
3011
3459
  if (typeof p.baseUrl === "string" && p.baseUrl.includes("/proxy/"))
@@ -3013,7 +3461,7 @@ function prepareConfigForSave(instanceId, config) {
3013
3461
  const apiKey = p.apiKey;
3014
3462
  delete p.apiKey;
3015
3463
  if (envFiles.length) {
3016
- envUpdates[inferProviderApiKeyEnvName(providerId)] = String(apiKey || "");
3464
+ envUpdates[inferProviderApiKeyEnvName(providerId)] = decryptRuntimeProviderApiKey(apiKey);
3017
3465
  }
3018
3466
  else {
3019
3467
  p.apiKey = apiKey;
@@ -3036,6 +3484,56 @@ function prepareConfigForSave(instanceId, config) {
3036
3484
  }
3037
3485
  return [configToWrite, envUpdates];
3038
3486
  }
3487
+ function decryptRuntimeProviderApiKey(apiKey) {
3488
+ const value = String(apiKey || "");
3489
+ return value.startsWith("enc:") ? decryptApiKey(value) : value;
3490
+ }
3491
+ function decryptRuntimeProviderEnv(env) {
3492
+ for (const [key, value] of Object.entries(env)) {
3493
+ if (!key.endsWith("_API_KEY"))
3494
+ continue;
3495
+ if (!String(value || "").startsWith("enc:"))
3496
+ continue;
3497
+ env[key] = decryptApiKey(value);
3498
+ }
3499
+ }
3500
+ /**
3501
+ * Auto-injects provider-specific host env vars that OpenClaw tools need
3502
+ * at runtime, if the user hasn't already set them explicitly.
3503
+ *
3504
+ * Currently handles:
3505
+ * - MINIMAX_API_HOST: Derived from MINIMAX_BASE_URL or upstream proxy config
3506
+ * to ensure the VLM tool calls the correct MiniMax API domain.
3507
+ *
3508
+ * Note: Only MiniMax needs this because its VLM tool reads MINIMAX_API_HOST
3509
+ * to determine the endpoint. DeepSeek does not have a dedicated VLM tool
3510
+ * in OpenClaw that requires a similar host env var.
3511
+ */
3512
+ function injectProviderHostEnv(env, instanceId) {
3513
+ // MINIMAX_API_HOST — needed by OpenClaw's VLM tool to call the correct domain
3514
+ if (!env.MINIMAX_API_HOST) {
3515
+ const baseUrl = env.MINIMAX_BASE_URL || "";
3516
+ if (baseUrl) {
3517
+ try {
3518
+ const parsed = new URL(baseUrl);
3519
+ env.MINIMAX_API_HOST = `${parsed.protocol}//${parsed.host}`;
3520
+ }
3521
+ catch { /* invalid URL, skip */ }
3522
+ }
3523
+ else {
3524
+ // Infer from upstream proxy config if available
3525
+ const config = getProxyInstanceConfig(instanceId);
3526
+ const upstream = config?.["x-jishushell"]?.proxy?.upstream;
3527
+ if (upstream?.providerId === "minimax" && upstream.baseUrl) {
3528
+ try {
3529
+ const parsed = new URL(upstream.baseUrl);
3530
+ env.MINIMAX_API_HOST = `${parsed.protocol}//${parsed.host}`;
3531
+ }
3532
+ catch { /* invalid URL, skip */ }
3533
+ }
3534
+ }
3535
+ }
3536
+ }
3039
3537
  /**
3040
3538
  * Dissociate a cloned/imported config from its source instance's IM bindings.
3041
3539
  * Physically migrated from `instance-manager.stripImBindings` so framework