Skip to content

Fix #2245: bug: masked apiKey written to config.yaml is never resolved on daemon restart — - #2246

Open
Memtensor-AI wants to merge 2 commits into
MemTensor:dev-v2.0.30from
Memtensor-AI:bugfix/autodev-2245-20260813223840660
Open

Fix #2245: bug: masked apiKey written to config.yaml is never resolved on daemon restart — #2246
Memtensor-AI wants to merge 2 commits into
MemTensor:dev-v2.0.30from
Memtensor-AI:bugfix/autodev-2245-20260813223840660

Conversation

@Memtensor-AI

Copy link
Copy Markdown
Collaborator

Description

Fixed #2245 — masked apiKey never resolved on daemon restart. The bridge persists config.yaml with API keys masked to __memos_secret__ via maskSecrets() and strips empty secrets from patches via stripEmptySecrets(), but nothing re-reads the real value back. On restart, loadConfig() treated the mask as the literal API key, every LLM call failed auth, and the bridge restart-looped with lastOkAt: null and skill.crystallize.failed ... timed out after 120000 ms.

Solution (read-side only, on-disk mask preserved): resolveConfig() in apps/memos-local-plugin/core/config/index.ts now walks SECRET_FIELD_PATHS after pruneUnknown and before deepMerge, resolving each leaf via a new resolveSecretEnv() helper. ${VAR} references expand from process.env when the name matches the allowlist `^[A-Z][A-Z0-9_]*_(API_KEY|TOKEN)## Description

(non-allowlisted names emit a warning and stay untouched). __memos_secret__ and empty-string apiKey leaves fall back to LLM_API_KEY (or EMBEDDING_API_KEY for embedding.apiKey), then to OPENCODE_GO_API_KEY / OPENCODE_ZEN_API_KEY for LLM-class fields only — embedding never inherits an LLM provider's key. Hub tokens (hub.teamToken, hub.userToken) require an explicit ${VAR}. Real values pass through unchanged; the caller's raw config object is never mutated. Because resolveConfig() is the single choke point for both disk-loaded and in-memory patched configs, this covers both paths.

Tests: added tests/unit/config/resolve-secret-env.test.ts with 10 unit tests covering ${VAR} expansion, mask sentinel resolution, empty-string fallback, embedding vs LLM channel isolation across all SECRET_FIELD_PATHS, hub-token ${VAR} path, allowlist enforcement (warning + no expansion for ${HOME}), non-mutation of the raw config, and negative cases (no env → mask retained; unset ${VAR} → literal preserved; real values untouched). npx vitest run tests/unit/config → 5 files / 71 tests passed. npx tsc -p tsconfig.json --noEmit clean.

Note: PR #2235 already proposes the same read-side fix; this branch mirrors that approach and is ready to supersede or replace it. Reviewers: @whipser030, @hijzy.

Related Issue (Required): Fixes #2245

Type of change

Please delete options that are not relevant.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Refactor (does not change functionality, e.g. code style improvements, linting)
  • Documentation update

How Has This Been Tested?

Automated tests are pending.

  • Unit Test
  • Test Script Or Test Steps (please provide)
  • Pipeline Automated API Test (please provide)

Checklist

  • I have performed a self-review of my own code
  • I have commented my code in hard-to-understand areas
  • I have added tests that prove my fix is effective or that my feature works
  • I have created related documentation issue/PR in MemOS-Docs (if applicable)
  • I have linked the issue to this PR (if applicable)
  • I have mentioned the person who will review this PR

@whipser030, @hijzy please review this PR.

Reviewer Checklist

Bridge persists config.yaml with API keys masked to __memos_secret__
via maskSecrets() and strips empty secrets from patches via
stripEmptySecrets(), but nothing re-reads the real value back. On
daemon restart, loadConfig() treats the mask as the literal API key,
every LLM call fails auth, and the bridge restart-loops with
lastOkAt: null and skill.crystallize stuck.

Make resolveConfig() (the single choke point for both disk-loaded
and in-memory patched configs) walk SECRET_FIELD_PATHS after
pruneUnknown and before deepMerge:

- ${VAR} references resolve from process.env when the name matches
  the allowlist ^[A-Z][A-Z0-9_]*_(API_KEY|TOKEN)$; other names emit
  a warning and stay untouched.
- __memos_secret__ / empty apiKey leaves fall back to LLM_API_KEY
  (or EMBEDDING_API_KEY for embedding.apiKey), then to
  OPENCODE_GO_API_KEY / OPENCODE_ZEN_API_KEY for LLM-class fields
  only. Embedding never inherits an LLM provider's key.
- Hub tokens (hub.teamToken, hub.userToken) require an explicit
  ${VAR} — no path-based env convention.
- Real values pass through unchanged; the caller's raw config
  object is never mutated (resolution runs on the pruneUnknown copy).

Read-side only: on-disk write stays masked, so the security posture
of maskSecrets() is preserved.

Adds 10 unit tests under tests/unit/config/resolve-secret-env.test.ts
covering ${VAR} expansion, mask sentinel resolution, empty-string
fallback, per-path env conventions (embedding vs LLM channel
isolation), hub token ${VAR} path, allowlist enforcement,
non-mutation of the raw config, and negative cases (no env → mask
retained; unset ${VAR} → literal preserved; real values untouched).

Fixes MemTensor#2245
@Memtensor-AI Memtensor-AI added ai:generated Generated or modified by AI | 由 AI 生成或修改 area:plugin OpenClaw & Hermes status:in-progress Someone or AI is working on it | 人工或 AI 正在处理 labels Aug 13, 2026
@Memtensor-AI

Memtensor-AI commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

🤖 Open Code Review

Target: PR #2246
Task: 152560a35a72794c
Base: dev-v2.0.30
Head: bugfix/autodev-2245-20260813223840660
Head SHA: 1f28c8c57ee29a880ad2ca0bfa9754f860e89cbd

OpenCodeReview: No comments generated. Looks good to me.

Generated by cloud-assistant via Open Code Review.

@Memtensor-AI

Copy link
Copy Markdown
Collaborator Author

🔧 Open Code Review requested Agent fix

Open Code Review found 4 issue(s). I have resumed the development Agent to fix them.

  • Task: 152560a35a72794c
  • Fix attempt: 1/2
  • Finding delta: 0 repeated / 4 new / 0 likely resolved

The Agent will push a new commit to this PR branch. OCR will recheck after the commit is pushed.

Address 4 findings from the open-code-review pass on PR MemTensor#2246:

1. hub.teamToken / hub.userToken are now resolved from the environment
   when masked with __memos_secret__ or written as empty strings. The
   previous `if (leaf !== "apiKey") continue` short-circuit silently
   perpetuated the original bug for hub tokens.

2. Emit a warning when a secret leaf references an env var that is not
   set (both the explicit ${VAR} form and the mask/empty form). Without
   this, a user who writes `apiKey: ${MY_API_KEY}` and forgets to export
   MY_API_KEY sees auth failures with no actionable log line.

3. Restrict the OPENCODE_GO_API_KEY / OPENCODE_ZEN_API_KEY generic
   fallback to the primary llm.apiKey. Per-component overrides
   (l3Llm.apiKey, skillEvolver.apiKey) and non-LLM secrets
   (embedding.apiKey, hub.*Token) must never silently borrow an
   unrelated provider's key — that causes cross-provider auth failures
   and unexpected billing when those components are pointed at a
   different provider than the shared llm settings.

4. Use an explicit `traversalOk` flag when walking SECRET_FIELD_PATHS
   so a partial traversal cannot leave `cursor` pointing at a
   shallower valid intermediate node that would then pass the
   isPlainObject check and cause `leaf` to be looked up on the wrong
   object. Today every entry is 2 levels deep so the bug is latent,
   but the flag makes the intent explicit and future-proofs against
   deeper paths being added.

Env var derivation for masked/empty leaves now uses a camel→SNAKE
transform on the last two path segments so every SECRET_FIELD_PATHS
entry is resolvable by convention:

  embedding.apiKey    → EMBEDDING_API_KEY
  llm.apiKey          → LLM_API_KEY
  l3Llm.apiKey        → L3_LLM_API_KEY
  skillEvolver.apiKey → SKILL_EVOLVER_API_KEY
  hub.teamToken       → HUB_TEAM_TOKEN
  hub.userToken       → HUB_USER_TOKEN

Tests updated to cover the new hub-token resolution, the tightened
fallback scope, and both warning cases. All 76 config tests pass;
tsc --noEmit clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Memtensor-AI

Copy link
Copy Markdown
Collaborator Author

✅ Automated Test Results: PASSED

All tests passed (50/50 executed). memos_local_plugin/unit: 10/10, memos_python_core/changed-repo-python: 40/40. Duration: 10s [advisory, non-gating] AI-generated tests on branch test/auto-gen-152560a35a72794c-20260814065258: 146/149 passed, 3 failed — these do NOT affect the PR verdict; review the branch manually.

Branch: bugfix/autodev-2245-20260813223840660

@Memtensor-AI Memtensor-AI added status:ready Ready for implementation; waiting for assignee or AI dispatch | 可进入实现,等待认领或派发 and removed status:in-progress Someone or AI is working on it | 人工或 AI 正在处理 labels Aug 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai:generated Generated or modified by AI | 由 AI 生成或修改 area:plugin OpenClaw & Hermes status:ready Ready for implementation; waiting for assignee or AI dispatch | 可进入实现,等待认领或派发

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants