Skip to content

fix: avoid false hardcoded API key warning for env interpolation#1

Open
100yenadmin wants to merge 1 commit into
mainfrom
sub/fix-plugin-secrets-1701
Open

fix: avoid false hardcoded API key warning for env interpolation#1
100yenadmin wants to merge 1 commit into
mainfrom
sub/fix-plugin-secrets-1701

Conversation

@100yenadmin
Copy link
Copy Markdown
Member

@100yenadmin 100yenadmin commented Mar 30, 2026

Summary

  • detect ${VAR} API key config from the raw plugin config before env resolution
  • keep warning for truly hardcoded API keys, but suppress the false positive for env-backed config
  • add a regression test covering both interpolated and hardcoded cases

Verification

  • npm run build
  • node --test tests/api-key-warning.test.mjs

Context

Fixes the false warning reported in 100yenadmin/electric-sheep#1701 when users follow the recommended ~/.openclaw/.env + ${CORTEX_API_KEY} setup.


Open with Devin

Copilot AI review requested due to automatic review settings March 30, 2026 11:33
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses a false-positive security warning in the Cortex OpenClaw plugin by distinguishing ${ENV_VAR} interpolation from truly hardcoded API keys before environment resolution.

Changes:

  • Detect ${VAR} interpolation using the raw plugin config value (pre-resolveEnv) to suppress the warning for env-backed keys.
  • Keep the warning for truly hardcoded API keys (with a new “warn once per process” guard).
  • Add a regression test covering both interpolated and hardcoded API key cases.

Reviewed changes

Copilot reviewed 2 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/index.ts Adds isEnvInterpolation() and updates hardcoded-key warning logic to consult raw config before env resolution.
tests/api-key-warning.test.mjs Adds regression tests for interpolated vs hardcoded apiKey warning behavior.
dist/index.js Compiled output reflecting the updated warning logic.
dist/index.js.map Updated sourcemap for the compiled JS.
dist/index.d.ts.map Updated declaration sourcemap for the compiled types.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

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);
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

loadPlugin() builds a file URL via pathToFileURL(new URL(...).pathname). Using .pathname can break on Windows (leading /C:/...) and loses URL encoding info. Prefer using the new URL('../dist/index.js', import.meta.url) result directly (or pathToFileURL(fileURLToPath(...)) if you need a path) so the test is portable across platforms.

Suggested change
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);

Copilot uses AI. Check for mistakes.
Comment on lines +23 to +31
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);
Copy link

Copilot AI Mar 30, 2026

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.

Suggested change
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;
}
}

Copilot uses AI. Check for mistakes.
Comment thread src/index.ts
Comment on lines +31 to +33
// Module-level flag: emit hardcoded-key warning only once per process lifetime
let _apiKeyWarningEmitted = false;

Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new module-level _apiKeyWarningEmitted changes behavior to only warn once per process lifetime. This is not mentioned in the PR description and can suppress warnings if the plugin is re-registered later with a hardcoded key (or in multi-tenant scenarios). If this is intentional, consider calling it out in the PR description and/or scoping the flag to a plugin instance/config so each distinct registration can still emit the warning when appropriate.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 potential issues.

Open in Devin Review

Comment thread src/index.ts
import { dirname, join } from "node:path";

// Module-level flag: emit hardcoded-key warning only once per process lifetime
let _apiKeyWarningEmitted = false;
Copy link
Copy Markdown

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 _apiKeyWarningEmitted flag at src/index.ts:32 is module-scoped, meaning once set by the first register() 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.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +5 to +10
async function loadPlugin() {
const url = pathToFileURL(new URL('../dist/index.js', import.meta.url).pathname);
url.searchParams.set('t', `${Date.now()}-${Math.random()}`);
const mod = await import(url.href);
return mod.default?.default ?? mod.default ?? mod;
}
Copy link
Copy Markdown

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.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread src/index.ts
Comment on lines +93 to +95
function isEnvInterpolation(value: unknown): value is string {
return typeof value === "string" && /^\$\{[^}]+\}$/.test(value.trim());
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: isEnvInterpolation only matches full-string interpolation, not partial

The regex /^\$\{[^}]+\}$/ at src/index.ts:94 requires the entire trimmed string to be a single ${...} token. Partial interpolation like "prefix-${KEY}" or "${KEY1}-${KEY2}" would NOT match, causing the hardcoded-key warning to fire. This is reasonable behavior — partial interpolation of API keys is atypical and arguably a misconfiguration worth flagging — but it's worth noting that resolveEnv() at src/index.ts:89-91 supports partial/multiple interpolations via its global .replace() regex.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants