LibreChat's PluginAuth schema keys on a string userId; Key (the chat key
collection) is ObjectId-typed and Mongoose casts for it. The seeder used
`user._id` (an ObjectId) for BOTH, so every MCP auth row it wrote was
invisible to PluginService.getUserPluginAuthValue():
string lookup -> 0 rows (what the app does)
ObjectId lookup-> 1 row
Consequence: userMCPAuthMap['mcp_inference-image'] was empty for every
member, ToolService skipped the server ("missing user configuration"), and
any agent expecting MCP tools failed with
AgentToolInitializationError: "The agent is configured to use MCP tools, but
none are available" — 7 times since Oct 19, most recently 2026-10-27 22:29.
The image-generation MCP server (generate_image) was unavailable to everyone.
The docs server was unaffected (it declares no customUserVars).
Fix: String(uid) in the two pluginauths upserts only. The keys[] upserts are
deliberately left as ObjectId — chat key lookup works and changing it would
break the working half.
Verified: 54 existing rows migrated ObjectId -> string (backup at
/app/data/backup-pluginauths-userid-2026-10-27T2239Z.json), all 55 image rows
now string-typed, the app-style string lookup returns a row, and a full
seeder run passes its own decrypt guard and encrypt/decrypt round-trip check
(54 seeded, 17 skipped).
Three drifts between this repo and reality, found while moving the per-member key
seed into the app:
- The 'verify after every image deploy' block asserted the CREAM palette
(rgb(250,248,245)). The deployed palette has been the green wash
(rgb(234,243,234) with white text areas) for some time, so a correct deploy
would have read as a failure — and invited someone to 'fix' the CSS back.
- The block still verified against librechat-test.inference.coop; the swap to
chat.inference.coop happened 2026-10-21 and that alias is not provisioned.
- Nothing checked that the scheduler task's script exists in the running image,
which is exactly the failure mode a manifest repoint can introduce.
release.sh encodes the documented release flow (dirty-tree guard, always
--no-cache, pin-before-deploy) and its verify subcommand runs checks 1-5 plus the
seed task's log lines. sync-keys.js now prints its own md5 prefix on every run,
so a stale volume copy can never hide again — the repo copy's md5 is the
authority.
Verified: ./release.sh verify passes all six checks against the live app;
./release.sh volume sync-keys.js confirms md5 parity.
The first scheduled run failed closed (good) — Cloudron's scheduler addon runs
each task in its OWN container, so the app's process env is absent there and
/proc/1/environ belongs to the task launcher, not to LibreChat. CREDS_KEY/CREDS_IV
now come from /app/data/.librechat-creds (root:cloudron 0640), written from the
live app env; a stale key makes the decrypt-against-existing-rows guard refuse to
write rather than corrupt member keys. Credential failure now also writes
/app/data/key-sync-diag.txt so a future failure explains itself.
Verified: the 22:40 scheduled fire logged 'creds check: decrypt-ok (25 existing
row(s))' / 'done: 50 seeded, 15 skipped'.
The per-member key seed ran from a steward cron on the co-op agent VM, shelling
into this app over the Cloudron API. That made a member-facing function depend on
a separate machine's uptime and paths (it went silently stale once, exiting 127
on schedule while the job looked healthy).
- CloudronManifest.json: addons.scheduler gains librechat_key_sync
(cd /app/code/librechat/api && node /app/data/sync-keys.js, */10 * * * *).
Official addon set (postgresql, mongodb, localstorage, oidc, sendmail) and the
dockerImage pin are untouched.
- sync-keys.js: refuse to write unless CREDS_KEY can first decrypt an existing
key row, and unless each fresh ciphertext round-trips back to the plaintext.
Scheduled runs get a different environment than an interactive exec, so a wrong
or absent CREDS_KEY must fail loudly instead of overwriting working member keys.