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.
- package/Dockerfile.hermes-slim +2 -5
- package/apps/filebrowser-container.yaml +1 -0
- package/apps/ollama-binary.yaml +44 -0
- package/apps/ollama-with-hollama-binary.yaml +45 -1
- package/dist/cli/doctor.js +144 -16
- package/dist/cli/doctor.js.map +1 -1
- package/dist/install.js +1 -1
- package/dist/install.js.map +1 -1
- package/dist/routes/instances.js +42 -5
- package/dist/routes/instances.js.map +1 -1
- package/dist/routes/llm.js +29 -0
- package/dist/routes/llm.js.map +1 -1
- package/dist/server.js +18 -4
- package/dist/server.js.map +1 -1
- package/dist/services/agent-apps/catalog.d.ts +3 -0
- package/dist/services/agent-apps/catalog.js +40 -13
- package/dist/services/agent-apps/catalog.js.map +1 -1
- package/dist/services/agent-apps/installers/shell-script.d.ts +1 -1
- package/dist/services/agent-apps/installers/shell-script.js +19 -2
- package/dist/services/agent-apps/installers/shell-script.js.map +1 -1
- package/dist/services/agent-apps/types.d.ts +3 -0
- package/dist/services/app/app-manager.d.ts +8 -0
- package/dist/services/app/app-manager.js +77 -3
- package/dist/services/app/app-manager.js.map +1 -1
- package/dist/services/app/openclaw-manager.js +17 -2
- package/dist/services/app/openclaw-manager.js.map +1 -1
- package/dist/services/backup-manager.js +43 -4
- package/dist/services/backup-manager.js.map +1 -1
- package/dist/services/capability-endpoint-validator.js +26 -7
- package/dist/services/capability-endpoint-validator.js.map +1 -1
- package/dist/services/instance-manager.js +89 -9
- package/dist/services/instance-manager.js.map +1 -1
- package/dist/services/llm-proxy/index.d.ts +28 -0
- package/dist/services/llm-proxy/index.js +76 -3
- package/dist/services/llm-proxy/index.js.map +1 -1
- package/dist/services/llm-proxy/validate-key.d.ts +41 -0
- package/dist/services/llm-proxy/validate-key.js +672 -0
- package/dist/services/llm-proxy/validate-key.js.map +1 -0
- package/dist/services/macos-launchd.d.ts +89 -0
- package/dist/services/macos-launchd.js +273 -0
- package/dist/services/macos-launchd.js.map +1 -0
- package/dist/services/nomad-manager.d.ts +7 -0
- package/dist/services/nomad-manager.js +290 -79
- package/dist/services/nomad-manager.js.map +1 -1
- package/dist/services/panel-manager.js +20 -10
- package/dist/services/panel-manager.js.map +1 -1
- package/dist/services/runtime/adapters/custom.js +56 -0
- package/dist/services/runtime/adapters/custom.js.map +1 -1
- package/dist/services/runtime/adapters/hermes.d.ts +4 -3
- package/dist/services/runtime/adapters/hermes.js +165 -63
- package/dist/services/runtime/adapters/hermes.js.map +1 -1
- package/dist/services/runtime/adapters/openclaw.d.ts +28 -0
- package/dist/services/runtime/adapters/openclaw.js +502 -4
- package/dist/services/runtime/adapters/openclaw.js.map +1 -1
- package/dist/services/setup-manager.js +97 -50
- package/dist/services/setup-manager.js.map +1 -1
- package/dist/services/update-manager.js +32 -14
- package/dist/services/update-manager.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/install/jishu-install.sh +247 -35
- package/install/jishu-uninstall.sh +45 -5
- package/package.json +5 -2
- package/public/assets/ApiKeyField-CvyAOcJS.js +1 -0
- package/public/assets/Dashboard-AuJESBlJ.js +1 -0
- package/public/assets/{HermesChatPanel-B_2HlVBQ.js → HermesChatPanel-CByPREwb.js} +1 -1
- package/public/assets/HermesConfigForm-DRda8FKX.js +4 -0
- package/public/assets/InitPassword-ka4wNpM5.js +1 -0
- package/public/assets/InstanceDetail-Cg1nS8HX.js +92 -0
- package/public/assets/Login-aPajuQzf.js +1 -0
- package/public/assets/NewInstance-Dd1ebNIx.js +1 -0
- package/public/assets/ProviderRecommendations-DFmADQ7V.js +1 -0
- package/public/assets/Settings-BYQnbLYL.js +1 -0
- package/public/assets/Setup-D05lwDOV.js +1 -0
- package/public/assets/WeixinLoginPanel-D89kdhP4.js +9 -0
- package/public/assets/index-HSXCsceK.css +1 -0
- package/public/assets/{index-BZc5zH7u.js → index-bnBu0nlQ.js} +7 -7
- package/public/assets/registry-C_qeFTkZ.js +2 -0
- package/public/assets/usePolling-Bn93fe7M.js +1 -0
- package/public/assets/{vendor-i18n-y9V7Sfuu.js → vendor-i18n-flxcMVeP.js} +2 -2
- package/public/assets/{vendor-react-BWrEVJVb.js → vendor-react-ZC5T_huj.js} +1 -1
- package/public/index.html +4 -4
- package/scripts/check-colima-launchd.mjs +230 -0
- package/public/assets/Dashboard-BdWPtroF.js +0 -1
- package/public/assets/HermesConfigForm-DVlhg3WV.js +0 -4
- package/public/assets/InitPassword-D7glTExX.js +0 -1
- package/public/assets/InstanceDetail-CxSy2cpe.js +0 -92
- package/public/assets/Login-Cfr5c2sv.js +0 -1
- package/public/assets/NewInstance-BIYDmJis.js +0 -1
- package/public/assets/ProviderRecommendations-BuRnvRcI.js +0 -1
- package/public/assets/Settings-Cc-tYBil.js +0 -1
- package/public/assets/Setup-lGZEk5jq.js +0 -1
- package/public/assets/WeixinLoginPanel-CoGqzxeV.js +0 -9
- package/public/assets/index-87IJXG-w.css +0 -1
- package/public/assets/input-paste-CrNVAyOy.js +0 -1
- package/public/assets/providers-DtNXh9JD.js +0 -1
- package/public/assets/registry-BWnkJgZ1.js +0 -2
- 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
|
-
// ──
|
|
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)] =
|
|
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
|