limbo-ai 2026.4.2 → 2026.4.3
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 +5 -3
- package/cli.js +17 -9
- package/package.json +1 -1
- package/scripts/entrypoint.sh +44 -23
- package/scripts/patch-openclaw-audio.mjs +5 -6
- package/setup-server/server.js +3 -0
package/Dockerfile
CHANGED
|
@@ -42,7 +42,7 @@ ARG OPENCLAW_VERSION
|
|
|
42
42
|
# tini — minimal init for proper signal handling (PID 1 reaping)
|
|
43
43
|
# libssl3 — OpenSSL 3 shared lib needed by OpenClaw's ACP runtime (codex-acp)
|
|
44
44
|
# python3 — required by OpenClaw's pinned-write-helper for safe atomic file writes
|
|
45
|
-
RUN apt-get update && apt-get install -y --no-install-recommends gettext-base tzdata tini libssl3 python3 ca-certificates && rm -rf /var/lib/apt/lists/* \
|
|
45
|
+
RUN apt-get update && apt-get install -y --no-install-recommends gettext-base tzdata tini libssl3 python3 ca-certificates gosu && rm -rf /var/lib/apt/lists/* \
|
|
46
46
|
&& groupadd -r limbo && useradd --create-home -r -g limbo limbo
|
|
47
47
|
|
|
48
48
|
# Install OpenClaw globally — replaces the ZeroClaw Rust binary.
|
|
@@ -116,8 +116,10 @@ VOLUME ["/data"]
|
|
|
116
116
|
# OpenClaw gateway port
|
|
117
117
|
EXPOSE 18789
|
|
118
118
|
|
|
119
|
-
#
|
|
120
|
-
|
|
119
|
+
# Container starts as root — entrypoint.sh chowns data dirs then drops to
|
|
120
|
+
# non-root limbo user via gosu. This is the standard pattern used by
|
|
121
|
+
# PostgreSQL, Redis, and other Docker official images to handle bind-mount
|
|
122
|
+
# ownership mismatches between host and container users.
|
|
121
123
|
|
|
122
124
|
# tini as init process for proper signal forwarding and zombie reaping
|
|
123
125
|
ENTRYPOINT ["/usr/bin/tini", "--", "/entrypoint.sh"]
|
package/cli.js
CHANGED
|
@@ -32,6 +32,10 @@ const ENV_BACKUP_FILE = path.join(CONFIG_DIR, '.env.bak');
|
|
|
32
32
|
// The offset (not an absolute number) means multi-instance installs on
|
|
33
33
|
// custom ports don't collide.
|
|
34
34
|
const CONTROL_PORT_OFFSET = 2;
|
|
35
|
+
// Fixed wizard port for Google OAuth callback — every Limbo install uses the
|
|
36
|
+
// same port so a single redirect URI can be registered in Google Console.
|
|
37
|
+
// Override via LIMBO_WIZARD_PORT env var for edge cases (multi-instance).
|
|
38
|
+
const DEFAULT_WIZARD_PORT = 15789;
|
|
35
39
|
const COMPOSE_FILE = path.join(LIMBO_DIR, 'docker-compose.yml');
|
|
36
40
|
const DEFAULT_REGISTRY = 'registry.gitlab.com/tomas209/limbo';
|
|
37
41
|
const REGISTRY_IMAGE = process.env.LIMBO_REGISTRY || DEFAULT_REGISTRY;
|
|
@@ -170,27 +174,28 @@ function resolveExtraEnv() {
|
|
|
170
174
|
|
|
171
175
|
// docker-compose.yml written to ~/.limbo on install
|
|
172
176
|
function composeContent() {
|
|
177
|
+
const wizardPort = parseInt(process.env.LIMBO_WIZARD_PORT, 10) || DEFAULT_WIZARD_PORT;
|
|
173
178
|
return `services:
|
|
174
179
|
limbo:
|
|
175
180
|
image: ${resolveImage()}
|
|
176
181
|
init: true
|
|
177
182
|
restart: unless-stopped
|
|
178
183
|
read_only: true
|
|
179
|
-
security_opt:
|
|
180
|
-
- no-new-privileges:true
|
|
181
184
|
cap_drop:
|
|
182
185
|
- ALL
|
|
183
186
|
cap_add:
|
|
184
187
|
- CHOWN
|
|
185
188
|
- FOWNER
|
|
189
|
+
- SETUID
|
|
190
|
+
- SETGID
|
|
186
191
|
pids_limit: 200
|
|
187
192
|
tmpfs:
|
|
188
193
|
- /tmp:size=100M,noexec,nosuid,nodev
|
|
189
194
|
- /home/limbo/.npm:size=50M,noexec,nosuid,nodev,uid=999,gid=999
|
|
190
195
|
ports:
|
|
191
196
|
- "127.0.0.1:${PORT}:${PORT}"
|
|
192
|
-
# Wizard port (
|
|
193
|
-
- "127.0.0.1:${
|
|
197
|
+
# Wizard port (fixed for Google OAuth callback URI).
|
|
198
|
+
- "127.0.0.1:${wizardPort}:${wizardPort}"
|
|
194
199
|
# Control plane (LIMBO_PORT + 2) — host CLI talks to the supervisor here.
|
|
195
200
|
- "127.0.0.1:${PORT + CONTROL_PORT_OFFSET}:${PORT + CONTROL_PORT_OFFSET}"
|
|
196
201
|
volumes:
|
|
@@ -203,6 +208,7 @@ function composeContent() {
|
|
|
203
208
|
- ${ENV_FILE}
|
|
204
209
|
environment:
|
|
205
210
|
LIMBO_PORT: "${PORT}"
|
|
211
|
+
LIMBO_WIZARD_PORT: "${wizardPort}"
|
|
206
212
|
NODE_OPTIONS: "\${LIMBO_NODE_OPTIONS:---max-old-space-size=512}"
|
|
207
213
|
${resolveExtraEnv()} healthcheck:
|
|
208
214
|
test:
|
|
@@ -220,27 +226,28 @@ volumes:
|
|
|
220
226
|
|
|
221
227
|
// Hardened variant: adds Squid egress proxy sidecar with domain allowlist
|
|
222
228
|
function composeContentHardened() {
|
|
229
|
+
const wizardPort = parseInt(process.env.LIMBO_WIZARD_PORT, 10) || DEFAULT_WIZARD_PORT;
|
|
223
230
|
return `services:
|
|
224
231
|
limbo:
|
|
225
232
|
image: ${resolveImage()}
|
|
226
233
|
init: true
|
|
227
234
|
restart: unless-stopped
|
|
228
235
|
read_only: true
|
|
229
|
-
security_opt:
|
|
230
|
-
- no-new-privileges:true
|
|
231
236
|
cap_drop:
|
|
232
237
|
- ALL
|
|
233
238
|
cap_add:
|
|
234
239
|
- CHOWN
|
|
235
240
|
- FOWNER
|
|
241
|
+
- SETUID
|
|
242
|
+
- SETGID
|
|
236
243
|
pids_limit: 200
|
|
237
244
|
tmpfs:
|
|
238
245
|
- /tmp:size=100M,noexec,nosuid,nodev
|
|
239
246
|
- /home/limbo/.npm:size=50M,noexec,nosuid,nodev,uid=999,gid=999
|
|
240
247
|
ports:
|
|
241
248
|
- "127.0.0.1:${PORT}:${PORT}"
|
|
242
|
-
# Wizard port (
|
|
243
|
-
- "127.0.0.1:${
|
|
249
|
+
# Wizard port (fixed for Google OAuth callback URI).
|
|
250
|
+
- "127.0.0.1:${wizardPort}:${wizardPort}"
|
|
244
251
|
# Control plane (LIMBO_PORT + 2) — host CLI talks to the supervisor here.
|
|
245
252
|
- "127.0.0.1:${PORT + CONTROL_PORT_OFFSET}:${PORT + CONTROL_PORT_OFFSET}"
|
|
246
253
|
volumes:
|
|
@@ -253,6 +260,7 @@ function composeContentHardened() {
|
|
|
253
260
|
- ${ENV_FILE}
|
|
254
261
|
environment:
|
|
255
262
|
LIMBO_PORT: "${PORT}"
|
|
263
|
+
LIMBO_WIZARD_PORT: "${wizardPort}"
|
|
256
264
|
NODE_OPTIONS: "\${LIMBO_NODE_OPTIONS:---max-old-space-size=512}"
|
|
257
265
|
HTTP_PROXY: http://squid:3128
|
|
258
266
|
HTTPS_PROXY: http://squid:3128
|
|
@@ -2299,7 +2307,7 @@ function selfUpdateCli() {
|
|
|
2299
2307
|
|
|
2300
2308
|
if (isGlobal) {
|
|
2301
2309
|
log(`Updating CLI: ${pkg.version} → ${latest}...`);
|
|
2302
|
-
execSync('npm install -g limbo-ai@latest', { stdio: 'inherit', timeout:
|
|
2310
|
+
execSync('npm install -g limbo-ai@latest', { stdio: 'inherit', timeout: 300000 });
|
|
2303
2311
|
ok(`CLI updated to ${latest}.`);
|
|
2304
2312
|
try { fs.unlinkSync(UPDATE_CHECK_FILE); } catch {}
|
|
2305
2313
|
return true;
|
package/package.json
CHANGED
package/scripts/entrypoint.sh
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
|
-
# entrypoint.sh — Limbo container startup
|
|
2
|
+
# entrypoint.sh — Limbo container startup
|
|
3
|
+
# Runs as root. Fixes data-dir ownership, then drops to non-root limbo user
|
|
4
|
+
# via gosu (same pattern as PostgreSQL, Redis, and other Docker official images).
|
|
3
5
|
set -e
|
|
4
6
|
|
|
5
7
|
LOG_DIR="/data/logs"
|
|
@@ -16,12 +18,46 @@ log() {
|
|
|
16
18
|
|
|
17
19
|
log "INFO Limbo container starting"
|
|
18
20
|
|
|
21
|
+
# ── Fix data-dir ownership ───────────────────────────────────────────────────
|
|
22
|
+
# Bind-mounted dirs may be owned by the host user (uid ≠ 999). Named volumes
|
|
23
|
+
# may be root-owned. chown everything to limbo:limbo so the app can read/write.
|
|
24
|
+
chown -R limbo:limbo /data /home/limbo/.openclaw /home/limbo/.npm 2>/dev/null || true
|
|
25
|
+
log "INFO Data directory ownership fixed"
|
|
26
|
+
|
|
27
|
+
# ── Migrate legacy Docker secrets to .env ────────────────────────────────────
|
|
28
|
+
# Pre-v2026.4.3 composes mounted tokens as Docker secrets at /run/secrets/.
|
|
29
|
+
# The host CLI tried to migrate them but often failed because the secret files
|
|
30
|
+
# are root-owned and the CLI runs as a non-root host user. The container CAN
|
|
31
|
+
# read /run/secrets (root access before gosu), so we do it here — idempotent.
|
|
32
|
+
if [ -d /run/secrets ]; then
|
|
33
|
+
_env_file="/data/config/.env"
|
|
34
|
+
[ -f "$_env_file" ] || touch "$_env_file"
|
|
35
|
+
_migrated=0
|
|
36
|
+
for _pair in \
|
|
37
|
+
"telegram_bot_token:TELEGRAM_BOT_TOKEN" \
|
|
38
|
+
"groq_api_key:GROQ_API_KEY" \
|
|
39
|
+
"brave_api_key:BRAVE_API_KEY" \
|
|
40
|
+
"gateway_token:GATEWAY_TOKEN" \
|
|
41
|
+
"llm_api_key:LLM_API_KEY" \
|
|
42
|
+
"google_client_id:GOOGLE_CLIENT_ID" \
|
|
43
|
+
"google_client_secret:GOOGLE_CLIENT_SECRET"; do
|
|
44
|
+
_file="${_pair%%:*}"
|
|
45
|
+
_var="${_pair##*:}"
|
|
46
|
+
_path="/run/secrets/$_file"
|
|
47
|
+
if [ -f "$_path" ]; then
|
|
48
|
+
_val="$(cat "$_path" 2>/dev/null | tr -d '\n')"
|
|
49
|
+
if [ -n "$_val" ] && ! grep -q "^${_var}=" "$_env_file" 2>/dev/null; then
|
|
50
|
+
echo "${_var}=${_val}" >> "$_env_file"
|
|
51
|
+
_migrated=$((_migrated + 1))
|
|
52
|
+
fi
|
|
53
|
+
fi
|
|
54
|
+
done
|
|
55
|
+
[ "$_migrated" -gt 0 ] && log "INFO Migrated $_migrated token(s) from /run/secrets to .env"
|
|
56
|
+
fi
|
|
57
|
+
|
|
19
58
|
# ── Load config from .env ────────────────────────────────────────────────────
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
# secrets to read. Source it now so the rest of the script sees
|
|
23
|
-
# LLM_API_KEY / TELEGRAM_BOT_TOKEN / GROQ_API_KEY / BRAVE_API_KEY /
|
|
24
|
-
# GATEWAY_TOKEN as regular environment variables.
|
|
59
|
+
# All tokens live inside /data/config/.env. Source it now so the rest of the
|
|
60
|
+
# script sees LLM_API_KEY / TELEGRAM_BOT_TOKEN / etc. as environment variables.
|
|
25
61
|
OPENCLAW_STATE_DIR="${OPENCLAW_STATE_DIR:-/home/limbo/.openclaw}"
|
|
26
62
|
OPENCLAW_CONFIG_PATH="${OPENCLAW_CONFIG_PATH:-$OPENCLAW_STATE_DIR/openclaw.json}"
|
|
27
63
|
|
|
@@ -312,21 +348,6 @@ log "INFO Running migration runner"
|
|
|
312
348
|
node /app/migrations/index.js
|
|
313
349
|
log "INFO Migrations OK"
|
|
314
350
|
|
|
315
|
-
# ── Check workspace ownership ────────────────────────────────────────────────
|
|
316
|
-
# OpenClaw runs as uid=limbo but volumes persisted from older images may contain
|
|
317
|
-
# files owned by a different user (e.g. node:node). Detect and warn — the
|
|
318
|
-
# container can't chown without root, so we fail fast with a clear message.
|
|
319
|
-
WORKSPACE_DIR="${OPENCLAW_STATE_DIR}/workspace"
|
|
320
|
-
if [ -d "$WORKSPACE_DIR" ]; then
|
|
321
|
-
bad_file="$(find "$WORKSPACE_DIR" -not -user "$(id -u)" -print -quit 2>/dev/null)"
|
|
322
|
-
if [ -n "$bad_file" ]; then
|
|
323
|
-
log "ERROR Files in $WORKSPACE_DIR are not owned by limbo (uid=$(id -u))."
|
|
324
|
-
log "ERROR Example: $(ls -ln "$bad_file" 2>/dev/null | head -1)"
|
|
325
|
-
log "ERROR Fix from host: docker exec -u root <container> chown -R $(id -u):$(id -g) $WORKSPACE_DIR"
|
|
326
|
-
log "WARN Continuing anyway — OpenClaw may fail to write to some files"
|
|
327
|
-
fi
|
|
328
|
-
fi
|
|
329
|
-
|
|
330
351
|
# ── Export state dir for OpenClaw ─────────────────────────────────────────────
|
|
331
352
|
export OPENCLAW_STATE_DIR
|
|
332
353
|
export OPENCLAW_CONFIG_PATH
|
|
@@ -336,7 +357,7 @@ export OPENCLAW_CONFIG_PATH
|
|
|
336
357
|
# On restart, /data/config/.env exists → normal startup path.
|
|
337
358
|
if [ "$SETUP_MODE" = "true" ]; then
|
|
338
359
|
log "INFO No configuration found — starting setup wizard on port $LIMBO_PORT"
|
|
339
|
-
exec node /app/setup-server/server.js
|
|
360
|
+
exec gosu limbo node /app/setup-server/server.js
|
|
340
361
|
fi
|
|
341
362
|
|
|
342
363
|
# ── Clean up force-setup markers ─────────────────────────────────────────────
|
|
@@ -366,4 +387,4 @@ fi
|
|
|
366
387
|
LIMBO_CONTROL_PORT="${LIMBO_CONTROL_PORT:-$((LIMBO_PORT + 2))}"
|
|
367
388
|
export LIMBO_CONTROL_PORT
|
|
368
389
|
log "INFO Starting wizard supervisor (control plane: 127.0.0.1:${LIMBO_CONTROL_PORT})"
|
|
369
|
-
exec node /app/scripts/supervisor.js
|
|
390
|
+
exec gosu limbo node /app/scripts/supervisor.js
|
|
@@ -128,13 +128,12 @@ for (const file of files) {
|
|
|
128
128
|
continue;
|
|
129
129
|
}
|
|
130
130
|
if (!content.includes(ORIGINAL)) {
|
|
131
|
-
|
|
132
|
-
`
|
|
133
|
-
`
|
|
134
|
-
`
|
|
135
|
-
` against the new version and update this script, or drop it if upstream\n` +
|
|
136
|
-
` PR #64349 has landed.`
|
|
131
|
+
console.log(
|
|
132
|
+
`patch-openclaw-audio: skipping ${path.basename(file)} — code shape changed.\n` +
|
|
133
|
+
` OpenClaw likely updated past the bug. If Groq transcription breaks,\n` +
|
|
134
|
+
` re-verify the patch against the new version.`
|
|
137
135
|
);
|
|
136
|
+
continue;
|
|
138
137
|
}
|
|
139
138
|
const patchedContent = content.replace(ORIGINAL, REPLACEMENT);
|
|
140
139
|
if (patchedContent === content) {
|
package/setup-server/server.js
CHANGED
|
@@ -718,6 +718,9 @@ function handleGoogleOAuthStart(req, res) {
|
|
|
718
718
|
const pkce = generatePKCE();
|
|
719
719
|
const state = crypto.randomBytes(16).toString('hex');
|
|
720
720
|
const redirectUri = `http://localhost:${PORT}/auth/google/callback`;
|
|
721
|
+
// PORT is already the fixed wizard port (default 15789) passed by the
|
|
722
|
+
// supervisor via LIMBO_PORT env var. The redirect URI is deterministic
|
|
723
|
+
// so a single URI can be registered in Google Console for all installs.
|
|
721
724
|
|
|
722
725
|
googlePkceSession = { verifier: pkce.verifier, state, redirectUri, clientId, clientSecret, ts: Date.now() };
|
|
723
726
|
googleOauthResult = null;
|