fix(config): resolve masked apiKey from env on load - #2235
Conversation
🤖 Open Code ReviewTarget: PR #2235 🔍 OpenCodeReview found 2 issue(s) in this PR. 1.
|
|
Problem:
The bridge persists config.yaml with apiKey masked to __memos_secret__
(maskSecrets in memory-core.ts) and strips empty secrets from patches,
but nothing re-reads the real value back. On restart, loadConfig parses
the mask as the literal API key, so every LLM call fails auth and the
bridge restart-loops with lastOkAt: null while crystallize stays stuck
(observed with 290 candidate skills backlogged on 2026-08-11).
Solution:
resolveConfig now walks SECRET_FIELD_PATHS before merging and expands
placeholder values from the environment, read-side only:
- ${ENV_VAR} references are resolved from process.env
- __memos_secret__ / empty apiKey fields fall back to LLM_API_KEY,
EMBEDDING_API_KEY, then OPENCODE_GO_API_KEY / OPENCODE_ZEN_API_KEY
- hub tokens (teamToken/userToken) have no env convention and are
left untouched unless an explicit ${VAR} reference is used
- real values pass through unchanged; on-disk masking is preserved
Tests: 6 new unit tests in tests/unit/config/resolve-secret-env.test.ts
(env expansion, mask resolution, empty-string, all apiKey paths,
real-value passthrough, unset-env fallback). Full config suite 54/54.
cfeb5b7 to
3f33609
Compare
|
Thanks for the review — all four findings are fair, and I've folded them into the revised branch (now rebased onto the current main, 8d310a7). 1. Provider fallbacks leaking across secret fields (L126–L130) — agreed, good catch. The OPENCODE_GO/ZEN fallbacks were meant to spare opencode-go/zen users from defining a second env var, but applying them to 2. Unrestricted 3. In-place mutation of 4. Shadowed On the automated test run ("ENV ISSUE": Validation on the revised branch: config suite 71/71 (5 files — includes main's new Thanks again — the embedding-key leak in particular was a sharp catch. |
✅ Automated Test Results: PASSEDAll tests passed (10/10 executed). memos_local_plugin/unit: 10/10. Duration: 3s Branch: |
|
Deployment note (from the production deployment that hit this) The fix resolves masked keys from the daemon's process environment — which only works if the daemon actually has them. For systemd users: # /etc/systemd/system/memos-bridge.service.d/env.conf
[Service]
EnvironmentFile=/path/to/durable/bridge.envwhere
Env var contract implemented by this PR:
|
Summary
The bridge persists
config.yamlwith API keys masked to__memos_secret__(maskSecrets()incore/pipeline/memory-core.ts) and strips empty secrets from patches (stripEmptySecrets()). But nothing ever re-reads the real value back: when the daemon restarts,loadConfig()parses the mask as the literal API key, every LLM call fails auth, and the bridge restart-loops withlastOkAt: nullwhile skill crystallize stays stuck. This was observed live on 2026-08-11 with 290 candidate skills backlogged andskill.crystallize.failed ... openai_compatible timed out after 120000 msspam in the journal.This PR makes
resolveConfig()(the single choke point for both disk-loaded and in-memory patched configs) resolve masked/placeholder secret values from the environment, read-side only — the on-disk write stays masked, so the security posture ofmaskSecrets()is preserved.Change
apps/memos-local-plugin/core/config/index.ts—resolveConfig(raw)now walksSECRET_FIELD_PATHSbeforepruneUnknown/deepMerge:${ENV_VAR}references in any secret field resolve fromprocess.env[ENV_VAR]__memos_secret__/ empty-stringapiKeyfields fall back toLLM_API_KEY,EMBEDDING_API_KEY, thenOPENCODE_GO_API_KEY/OPENCODE_ZEN_API_KEYhub.teamToken,hub.userToken) have no env convention and are left untouched unless the user writes an explicit${VAR}Tests
New
tests/unit/config/resolve-secret-env.test.ts(6 tests):${ENV_VAR}expansion__memos_secret__mask resolutionapiKeySECRET_FIELD_PATHSleaves resolve (hub tokens stay masked)tsc -p tsconfig.json --noEmitpasses clean.Related
feat(l3): dedicated l3Llm config slot for abstraction pass— added thel3Llm.apiKeysecret path this PR also covers; without the env fallback, L3 abstraction fails withl3.abstraction: 'inference' must be an arraywhen the key is masked.fix(llm): preserve all vLLM stream chunks— same class of LLM-call robustness: this PR removes the auth-failure half of thetimed out after 120000 msburst seen oncapture.reflect.scoring(16-step) runs that overload single-slot local models.MemOS skillInjectionMode full but memos_skill_get never called— downstream symptom class where MemOS config/LLM wiring fails silently; this PR removes one root cause.Environment
@memtensor/memos-local-plugin2.0.12-beta.1 (source matchesapps/memos-local-plugin)https://opencode.ai/zen/go/v1,deepseek-v4-flash). Config written via the UI/viewer masks the key, restart loops,curlwith the env key works (GO_DEEPSEEK_OK), bridge with masked key fails.memos-bridge.serviceType of change
How Has This Been Tested?
vitest run tests/unit/config— 54 passed)lastOkAtpopulates and crystallize drains; verified 2026-08-11 on Hermes CT100)Checklist