Move the per-member key sync into the app (drop the dependency on the agent VM) #1

Closed
opened 2026-10-25 22:14:32 +00:00 by inference-bot · 1 comment
Owner

Problem

The per-member chat key seed (sync-keys.js) runs from outside this app: a steward cron on the co-op's agent VM shells into the Cloudron API, execs node /app/data/sync-keys.js here, and seeds each member's key into Mongo.

That makes a member-facing function depend on a separate machine:

  • A new member's first login has no key until the next external run — chat prompts for an API key and image generation reports a missing credential.
  • If the agent VM is down, misconfigured, or its paths drift, the seed stops and nothing in this app looks wrong. That happened tonight: the external job was firing exactly on schedule while exiting 127 (stale paths after a host migration), so new members would have hit the gap with no signal in the chat app.

This is the only remaining member-facing dependency on the agent VM. The other jobs there (status digest, steward updates, newsletter draft) are report-only.

Proposed change

Declare the seed as a Cloudron scheduler task inside this app, so it runs in the container on its own schedule with no external host and no Cloudron admin credential involved.

Sibling precedent: the member portal already runs membership_sweep and rag_refresh through the same addons.scheduler mechanism, so this is a pattern the co-op already operates, not a new one.

Staged steps — DO NOT RUN until the operator confirms

  1. Manifest (CloudronManifest.json): add to addons, keeping the file a copy of the official appstore manifest (currently postgresql, mongodb, localstorage, oidc, sendmail) plus the dockerImage repoint:

    "scheduler": {
      "librechat_key_sync": {
        "schedule": "*/10 * * * *",
        "command": "node /app/code/sync-keys.js"
      }
    }
    
  2. Dockerfile: COPY sync-keys.js /app/code/sync-keys.js so the script is versioned in the image instead of living only on the volume (today a volume restore loses it; the repo copy is the only other home).

  3. Deploy: cloudron builder build --no-cache → write the new tag into CloudronManifest.json.dockerImage → commit → cloudron update --app 4023ccbf-d57a-4500-97df-14de9cf3516e --force.

  4. Verify: run node /app/code/sync-keys.js by hand in the container first (foreground, expect the portal feed: N active member key(s) / done: N seeded, M skipped lines), then confirm the first scheduler fire in the app logs prints the same, then delete the external cron job.

Risks and open questions

  • Crash-loop risk is the real one. This app is member-facing and the overlay manifest must remain a copy of the official manifest except for the intended keys — a hand-edited manifest that drops an addon (postgresql, mongodb, oidc, sendmail, localstorage) crash-loops the app. Mitigation: byte-diff the manifest against the official one before building, and change only dockerImage + the new scheduler block.
  • Scheduler environment: Cloudron scheduler tasks run with the app's declared env, which does not include the sync token (it lives in /app/data/env on the volume). sync-keys.js already assembles its own env from /app/data/env + /proc/1/environ, which is why the external exec works — the first scheduler run has to prove the same holds. If it doesn't, the fallback is a two-line wrapper that sources /app/data/env before invoking node.
  • Interval: 10 minutes matches the current cadence; the gap it covers is only first-login, so 15–30 minutes would be equally fine and cheaper in noise.
  • Keep or drop the volume copy of sync-keys.js (/app/data/sync-keys.js)? Keeping it during transition lets the external job keep working until this is verified.
  • Separate, noticed while here: the appstore package for this app is now 1.4.5 (upstream LibreChat still 0.8.8); the overlay is pinned at 1.4.1. Worth deciding whether to fold that pin bump into the same rebuild or keep it as its own change.

Impact if done

No member-facing function depends on the agent VM. The seed runs in the app, on the app's own schedule, and cannot go silently stale because a machine elsewhere changed.

## Problem The per-member chat key seed (`sync-keys.js`) runs from **outside this app**: a steward cron on the co-op's agent VM shells into the Cloudron API, execs `node /app/data/sync-keys.js` here, and seeds each member's key into Mongo. That makes a **member-facing** function depend on a separate machine: - A new member's first login has no key until the next external run — chat prompts for an API key and image generation reports a missing credential. - If the agent VM is down, misconfigured, or its paths drift, the seed stops and **nothing in this app looks wrong**. That happened tonight: the external job was firing exactly on schedule while exiting 127 (stale paths after a host migration), so new members would have hit the gap with no signal in the chat app. This is the only remaining member-facing dependency on the agent VM. The other jobs there (status digest, steward updates, newsletter draft) are report-only. ## Proposed change Declare the seed as a **Cloudron scheduler task inside this app**, so it runs in the container on its own schedule with no external host and no Cloudron admin credential involved. Sibling precedent: the member portal already runs `membership_sweep` and `rag_refresh` through the same `addons.scheduler` mechanism, so this is a pattern the co-op already operates, not a new one. ## Staged steps — DO NOT RUN until the operator confirms 1. **Manifest** (`CloudronManifest.json`): add to `addons`, keeping the file a copy of the official appstore manifest (currently `postgresql`, `mongodb`, `localstorage`, `oidc`, `sendmail`) plus the `dockerImage` repoint: ```json "scheduler": { "librechat_key_sync": { "schedule": "*/10 * * * *", "command": "node /app/code/sync-keys.js" } } ``` 2. **Dockerfile**: `COPY sync-keys.js /app/code/sync-keys.js` so the script is versioned in the image instead of living only on the volume (today a volume restore loses it; the repo copy is the only other home). 3. **Deploy**: `cloudron builder build --no-cache` → write the new tag into `CloudronManifest.json.dockerImage` → commit → `cloudron update --app 4023ccbf-d57a-4500-97df-14de9cf3516e --force`. 4. **Verify**: run `node /app/code/sync-keys.js` by hand in the container first (foreground, expect the `portal feed: N active member key(s)` / `done: N seeded, M skipped` lines), then confirm the **first scheduler fire** in the app logs prints the same, then delete the external cron job. ## Risks and open questions - **Crash-loop risk is the real one.** This app is member-facing and the overlay manifest must remain a copy of the official manifest except for the intended keys — a hand-edited manifest that drops an addon (`postgresql`, `mongodb`, `oidc`, `sendmail`, `localstorage`) crash-loops the app. Mitigation: byte-diff the manifest against the official one before building, and change only `dockerImage` + the new `scheduler` block. - **Scheduler environment**: Cloudron scheduler tasks run with the app's declared env, which does **not** include the sync token (it lives in `/app/data/env` on the volume). `sync-keys.js` already assembles its own env from `/app/data/env` + `/proc/1/environ`, which is why the external exec works — the first scheduler run has to prove the same holds. If it doesn't, the fallback is a two-line wrapper that sources `/app/data/env` before invoking node. - **Interval**: 10 minutes matches the current cadence; the gap it covers is only first-login, so 15–30 minutes would be equally fine and cheaper in noise. - **Keep or drop the volume copy** of `sync-keys.js` (`/app/data/sync-keys.js`)? Keeping it during transition lets the external job keep working until this is verified. - **Separate, noticed while here**: the appstore package for this app is now `1.4.5` (upstream LibreChat still 0.8.8); the overlay is pinned at `1.4.1`. Worth deciding whether to fold that pin bump into the same rebuild or keep it as its own change. ## Impact if done No member-facing function depends on the agent VM. The seed runs in the app, on the app's own schedule, and cannot go silently stale because a machine elsewhere changed.
Author
Owner

Done and deployed — the seed now runs inside this app, and the external job is gone.

What changed

  • CloudronManifest.json — addons.scheduler gains librechat_key_sync (*/10 * * * *, cd /app/code/librechat/api && node /app/data/sync-keys.js). The official addon set and the dockerImage pin are untouched, exactly as the overlay rule requires.
  • sync-keys.js — fails closed: it writes nothing unless CREDS_KEY first decrypts an existing key row (proof it holds the app's real key), and unless each fresh ciphertext round-trips back to the plaintext. A credential problem exits non-zero and writes a diagnostic instead of touching member keys.
  • README.md — the scheduling, the credential source and the guards are documented as the runbook.
  • Commit 4e28b7a (manifest + guards), e544dc4 (creds file + diagnostic), 2d66ea2 (README).

Verified

  • Deploy: cloudron update --app 4023ccbf --force → snapshot taken, "App is updated", health check passed. GET /api/v1/apps/4023ccbf now returns addons.scheduler; chat.inference.coop health and login both HTTP 200; chat unaffected.
  • Keys intact after the deploy: creds check: decrypt-ok (25 existing row(s)), done: 50 seeded, 15 skipped (no sandbox account yet).

The interesting part, and why the guard earned its place

The first scheduled fire at 22:30 failed closed:

FATAL: CREDS_KEY/CREDS_IV not available — refusing to write (keys would be undecryptable)

Cloudron's scheduler addon runs each task in its own container — the app's process env is absent there, and /proc/1/environ belongs to the task launcher, not to LibreChat. So CREDS_KEY/CREDS_IV were simply not reachable, even though an interactive cloudron exec (which runs as root in the app container) sees them.

Without the guard, that run would have written undecryptable ciphertext over every working member key — a member-facing outage caused by a deployment that looked healthy. That is the whole reason the script now refuses to write on a credential mismatch.

Fix: the task reads CREDS_KEY/CREDS_IV from /app/data/.librechat-creds (root:cloudron, 0640), written once from the live app env — documented in the README, including how to refresh it if the app's key ever rotates.

The 22:40 scheduled fire, in the app's own log:

22:40:07 - creds check: decrypt-ok (25 existing row(s))
22:40:07 - portal feed: 65 active member key(s)
22:40:08 - done: 50 seeded, 15 skipped (no sandbox account yet)

External job removed — the bot cron and its copy of the script are deleted, so nothing member-facing depends on the agent VM any more.

Left open, deliberately:

  • sync-keys.js still runs from the volume. Shipping it in the image (COPY sync-keys.js /app/code/sync-keys.js) is the tidier end state, but this agent host has no docker and no registry login, so the build had to be skipped; the manifest deploy was done without one. Worth doing whenever someone is next on a machine with the build pipeline.
  • The appstore package is 1.4.5 while the overlay is pinned at 1.4.1 (upstream LibreChat still 0.8.8) — untouched here to keep this change verifiable on its own.
Done and deployed — the seed now runs inside this app, and the external job is gone. **What changed** - `CloudronManifest.json` — `addons.scheduler` gains `librechat_key_sync` (`*/10 * * * *`, `cd /app/code/librechat/api && node /app/data/sync-keys.js`). The official addon set and the `dockerImage` pin are untouched, exactly as the overlay rule requires. - `sync-keys.js` — fails closed: it writes nothing unless `CREDS_KEY` first decrypts an existing key row (proof it holds the app's real key), and unless each fresh ciphertext round-trips back to the plaintext. A credential problem exits non-zero and writes a diagnostic instead of touching member keys. - `README.md` — the scheduling, the credential source and the guards are documented as the runbook. - Commit `4e28b7a` (manifest + guards), `e544dc4` (creds file + diagnostic), `2d66ea2` (README). **Verified** - Deploy: `cloudron update --app 4023ccbf --force` → snapshot taken, "App is updated", health check passed. `GET /api/v1/apps/4023ccbf` now returns `addons.scheduler`; `chat.inference.coop` health and login both HTTP 200; chat unaffected. - Keys intact after the deploy: `creds check: decrypt-ok (25 existing row(s))`, `done: 50 seeded, 15 skipped (no sandbox account yet)`. **The interesting part, and why the guard earned its place** The **first scheduled fire at 22:30 failed closed**: ``` FATAL: CREDS_KEY/CREDS_IV not available — refusing to write (keys would be undecryptable) ``` Cloudron's scheduler addon runs each task in **its own container** — the app's process env is absent there, and `/proc/1/environ` belongs to the task launcher, not to LibreChat. So `CREDS_KEY`/`CREDS_IV` were simply not reachable, even though an interactive `cloudron exec` (which runs as root in the app container) sees them. Without the guard, that run would have written *undecryptable* ciphertext over every working member key — a member-facing outage caused by a deployment that looked healthy. That is the whole reason the script now refuses to write on a credential mismatch. Fix: the task reads `CREDS_KEY`/`CREDS_IV` from `/app/data/.librechat-creds` (`root:cloudron`, `0640`), written once from the live app env — documented in the README, including how to refresh it if the app's key ever rotates. **The 22:40 scheduled fire, in the app's own log:** ``` 22:40:07 - creds check: decrypt-ok (25 existing row(s)) 22:40:07 - portal feed: 65 active member key(s) 22:40:08 - done: 50 seeded, 15 skipped (no sandbox account yet) ``` **External job removed** — the bot cron and its copy of the script are deleted, so nothing member-facing depends on the agent VM any more. **Left open, deliberately:** - `sync-keys.js` still runs from the volume. Shipping it in the image (`COPY sync-keys.js /app/code/sync-keys.js`) is the tidier end state, but this agent host has no docker and no registry login, so the build had to be skipped; the manifest deploy was done without one. Worth doing whenever someone is next on a machine with the build pipeline. - The appstore package is `1.4.5` while the overlay is pinned at `1.4.1` (upstream LibreChat still 0.8.8) — untouched here to keep this change verifiable on its own.
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: co-op/librechat-custom#1