-
Notifications
You must be signed in to change notification settings - Fork 0
fix: avoid false hardcoded API key warning for env interpolation #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,6 +28,9 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; | |
| import { mkdirSync, existsSync } from "node:fs"; | ||
| import { dirname, join } from "node:path"; | ||
|
|
||
| // Module-level flag: emit hardcoded-key warning only once per process lifetime | ||
| let _apiKeyWarningEmitted = false; | ||
|
|
||
|
Comment on lines
+31
to
+33
|
||
| // Dynamic require for node:sqlite (available in Node 22+, avoids TS import issues) | ||
| let NodeDatabaseSync: any; | ||
| try { | ||
|
|
@@ -87,6 +90,10 @@ function resolveEnv(value: string): string { | |
| return value.replace(/\$\{([^}]+)\}/g, (_, key) => process.env[key] ?? ""); | ||
| } | ||
|
|
||
| function isEnvInterpolation(value: unknown): value is string { | ||
| return typeof value === "string" && /^\$\{[^}]+\}$/.test(value.trim()); | ||
| } | ||
|
Comment on lines
+93
to
+95
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📝 Info: isEnvInterpolation only matches full-string interpolation, not partial The regex Was this helpful? React with 👍 or 👎 to provide feedback. |
||
|
|
||
| function parseConfig(raw: unknown): EvaMemoryConfig { | ||
| const defaults: EvaMemoryConfig = { | ||
| cortexUrl: "http://localhost:8000", | ||
|
|
@@ -1012,9 +1019,13 @@ const cortexPlugin = { | |
| }, | ||
|
|
||
| register(api: OpenClawPluginApi) { | ||
| const rawConfig = api.pluginConfig && typeof api.pluginConfig === "object" && !Array.isArray(api.pluginConfig) | ||
| ? api.pluginConfig as Record<string, unknown> | ||
| : null; | ||
| const cfg = parseConfig(api.pluginConfig); | ||
|
|
||
| if (cfg.apiKey && !cfg.apiKey.startsWith("${")) { | ||
|
|
||
| if (cfg.apiKey && !isEnvInterpolation(rawConfig?.apiKey) && !_apiKeyWarningEmitted) { | ||
| _apiKeyWarningEmitted = true; | ||
| api.logger.warn("cortex: API key appears to be hardcoded in config. Consider using environment variable: apiKey: '${CORTEX_API_KEY}'"); | ||
| } | ||
|
|
||
|
|
@@ -1040,11 +1051,6 @@ const cortexPlugin = { | |
| api.logger.info("cortex: node:sqlite not available — local cache disabled"); | ||
| } | ||
|
|
||
| // Security: warn if API key is hardcoded in config instead of env var | ||
| if (cfg.apiKey && !cfg.apiKey.startsWith("${")) { | ||
| api.logger.warn("cortex: API key appears to be hardcoded in config. Consider using environment variable: apiKey: '${CORTEX_API_KEY}'"); | ||
| } | ||
|
|
||
| api.logger.info( | ||
| `cortex: registered (cortex=${cfg.cortexUrl}, owner=${cfg.ownerId}, recall=${cfg.autoRecall}, capture=${cfg.autoCapture}, shadow=${cfg.shadowMode})`, | ||
| ); | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,43 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import test from 'node:test'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import assert from 'node:assert/strict'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { pathToFileURL } from 'node:url'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async function loadPlugin() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const url = pathToFileURL(new URL('../dist/index.js', import.meta.url).pathname); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+3
to
+6
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { pathToFileURL } from 'node:url'; | |
| async function loadPlugin() { | |
| const url = pathToFileURL(new URL('../dist/index.js', import.meta.url).pathname); | |
| async function loadPlugin() { | |
| const url = new URL('../dist/index.js', import.meta.url); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📝 Info: Test cache busting may not isolate CJS module state, but tests pass regardless
The loadPlugin() function at tests/api-key-warning.test.mjs:6-9 appends a random query parameter to bust the ESM import cache. However, for CJS modules loaded via import(), Node.js may still resolve to the same require cache entry, sharing the module-level _apiKeyWarningEmitted state across test invocations. This doesn't cause test failures because: Test 1 (env-var config) never sets the flag (the isEnvInterpolation check short-circuits), so Test 2 (hardcoded config) still sees _apiKeyWarningEmitted === false and correctly fires the warning. The tests are order-independent for the same reason. However, if someone adds a test with a hardcoded key before the 'does not warn' test, and the cache busting doesn't work, the shared flag state could cause unexpected interactions.
Was this helpful? React with 👍 or 👎 to provide feedback.
Copilot
AI
Mar 30, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test sets process.env.CORTEX_API_KEY but never restores it. That can leak state into other tests when the suite grows or when tests are executed in a shared process. Save the previous value and restore it in a finally block or via test(..., (t) => { t.after(() => ...) })/afterEach.
| process.env.CORTEX_API_KEY = 'secret-from-env'; | |
| const warnings = []; | |
| const plugin = await loadPlugin(); | |
| plugin.register(makeApi( | |
| { apiKey: '${CORTEX_API_KEY}', cortexUrl: 'http://localhost:8000' }, | |
| { warn: (msg) => warnings.push(msg), info() {} }, | |
| )); | |
| assert.equal(warnings.some((msg) => msg.includes('hardcoded in config')), false); | |
| const previousApiKey = process.env.CORTEX_API_KEY; | |
| process.env.CORTEX_API_KEY = 'secret-from-env'; | |
| try { | |
| const warnings = []; | |
| const plugin = await loadPlugin(); | |
| plugin.register(makeApi( | |
| { apiKey: '${CORTEX_API_KEY}', cortexUrl: 'http://localhost:8000' }, | |
| { warn: (msg) => warnings.push(msg), info() {} }, | |
| )); | |
| assert.equal(warnings.some((msg) => msg.includes('hardcoded in config')), false); | |
| } finally { | |
| if (previousApiKey === undefined) { | |
| delete process.env.CORTEX_API_KEY; | |
| } else { | |
| process.env.CORTEX_API_KEY = previousApiKey; | |
| } | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📝 Info: Module-level _apiKeyWarningEmitted suppresses warnings across independent registrations
The
_apiKeyWarningEmittedflag atsrc/index.ts:32is module-scoped, meaning once set by the firstregister()call with a hardcoded key, ALL subsequent registrations — even for different agents or owners with different hardcoded keys — will silently skip the warning. This is intentional per the commit message (suppress subagent noise), but it means a genuinely misconfigured second registration in the same process would not be warned about. In practice this is unlikely since multi-registration typically uses the same config, but worth noting as a behavioral trade-off.Was this helpful? React with 👍 or 👎 to provide feedback.