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 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
- # Run as non-root limbo user
120
- USER limbo
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 (LIMBO_PORT + 1) supervisor spawns on-demand wizards here.
193
- - "127.0.0.1:${PORT + 1}:${PORT + 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 (LIMBO_PORT + 1) supervisor spawns on-demand wizards here.
243
- - "127.0.0.1:${PORT + 1}:${PORT + 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: 60000 });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "limbo-ai",
3
- "version": "2026.4.2",
3
+ "version": "2026.4.3",
4
4
  "description": "Your personal AI memory agent — install and manage Limbo via npx",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -1,5 +1,7 @@
1
1
  #!/bin/sh
2
- # entrypoint.sh — Limbo container startup (runs as user limbo)
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
- # After the secrets-consolidation refactor, all tokens live inside
21
- # /data/config/.env there are no separate secret files or Docker
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
- die(
132
- `target code not found in ${path.basename(file)}.\n` +
133
- ` The function declaration exists but its body no longer matches the\n` +
134
- ` expected shape. OpenClaw has probably been updated — re-verify the patch\n` +
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) {
@@ -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;