claude-flow
Version:
Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration
254 lines (221 loc) • 11.7 kB
JavaScript
/**
* Static guard for ruvnet/ruflo ADR-125 / ADR-130 env-var precedence pattern.
*
* Context
* -------
* ADR-125 (rvagent integration) and ADR-130 (graph intelligence backend)
* introduced several new env vars that configure runtime behaviour:
*
* CLAUDE_FLOW_MEMORY_PATH — override memory root directory
* CLAUDE_FLOW_DISABLE_BRIDGE — bypass AgentDB v3 bridge
* CLAUDE_FLOW_GRAPH_BACKEND — select graph backend (sqlite | agentdb)
* CLAUDE_FLOW_GRAPH_DECAY_RATE — default temporal decay rate
* CLAUDE_FLOW_EMBED_DIMS — embedding dimension override
*
* The project's documented resolution order for every config value is:
*
* CLI flag > ENV var > config-file > hardcoded default
*
* This audit scans the source tree for any env var read pattern that does NOT
* have a corresponding CLI-flag precedence guard (i.e., where `process.env`
* is the ONLY source of the value and no CLI argument can override it).
*
* Concretely it checks that every `process.env.CLAUDE_FLOW_*` read site
* either:
* (a) is inside a function that accepts an explicit argument (meaning the
* caller CAN pass a CLI-derived value and the env var is only a
* fallback), OR
* (b) has a comment containing "cli.*flag" / "argv" / "precedence" / "flag"
* documenting that a CLI flag takes precedence, OR
* (c) is a known opt-out env var (DISABLE_BRIDGE, SKIP_NPX — intentionally
* env-only because they are CI/test escape hatches, not user config).
*
* A violation means: a future contributor adds an env var and forgets to wire
* a CLI flag, silently making the CLI flag have no effect when the env var is
* set. That's the class of bug ADR-125 §"CLI flag wins" was written to prevent.
*
* Failure exits 1 with remediation instructions.
* CI wiring: .github/workflows/v3-ci.yml `env-var-precedence-audit` step in
* the `plugin-package-audit` job.
*/
import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join, resolve, relative } from 'node:path';
const __dirname = dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = resolve(__dirname, '..');
// ── Knob-controlled env vars that are intentionally env-only ─────────────────
// These are CI/test escape hatches or cross-process signals, not user config.
// They are explicitly exempt from the "CLI flag must win" requirement.
const KNOWN_ESCAPE_HATCHES = new Set([
// ── CI / test escape hatches ────────────────────────────────────────────────
'CLAUDE_FLOW_DISABLE_BRIDGE', // CI/test: force raw sql.js path — intentionally no CLI flag
'RUFLO_HOOK_SKIP_NPX', // CI: suppress cold-install latency in smoke tests
'RUFLO_SUBLINEAR_NATIVE', // Manual override for native vs WASM sublinear — CI/perf knob
// ── Feature flags (set by init into settings.json, not user-typed CLI) ──────
'CLAUDE_FLOW_V3_ENABLED',
'CLAUDE_FLOW_HOOKS_ENABLED',
'CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS',
// ── Process-internal / inter-process signalling ─────────────────────────────
'CLAUDE_FLOW_HEADLESS', // Set/read within same process invocation lifecycle
'CLAUDE_FLOW_FORCE_UPDATE', // Set by --force flag internally, then cleared — not external
'CLAUDE_FLOW_AUTO_UPDATE', // Auto-update cadence — env-only documented design
// ── Logging / diagnostics ───────────────────────────────────────────────────
'CLAUDE_FLOW_LOG_LEVEL',
'DEBUG',
'CLAUDE_FLOW_DEBUG',
// ── Provider credentials ─────────────────────────────────────────────────────
'ANTHROPIC_API_KEY',
'OPENAI_API_KEY',
'GOOGLE_API_KEY',
'CLAUDE_FLOW_ENCRYPTION_KEY', // Encryption key — credential, never a CLI flag
'RUFLO_GRAPH_INTELLIGENCE_WITNESS_KEY', // Ed25519 witness signing key — credential
'RUFLO_PROVIDER', // Provider selection in headless agent context
'PINATA_API_KEY',
'PINATA_API_SECRET',
'PINATA_API_JWT',
// ── Bootstrap / process-level bindings (can't chicken-egg with CLI parsing) ──
'CLAUDE_FLOW_CONFIG',
'CLAUDE_FLOW_MEMORY_BACKEND',
'CLAUDE_FLOW_MCP_PORT',
'CLAUDE_FLOW_MCP_HOST',
'CLAUDE_FLOW_MCP_TRANSPORT',
// ── CLI-flag-dominated env vars: documented precedence, large context window ─
// These have explicit precedence docs that appear >10 lines before the read.
// The audit's 10-line context window misses them; they are tracked here to
// prevent noisy false positives. Each must have the precedence documented
// in the source file (checked manually and confirmed below).
// CLAUDE_FLOW_MEMORY_PATH — memory-initializer.ts lines 19-28 doc
// "Precedence (highest → lowest): 1. CLAUDE_FLOW_MEMORY_PATH env var"
// See also memory.ts line 12: "#2105: --path > CLAUDE_FLOW_DB_PATH > CLAUDE_FLOW_MEMORY_PATH"
'CLAUDE_FLOW_MEMORY_PATH',
// ── OS / runtime standard env ────────────────────────────────────────────────
'HOME',
'USERPROFILE',
'CLAUDE_PROJECT_DIR',
'PATH',
'npm_config_prefix',
'npm_execpath',
'NODE_ENV',
'PROMPT',
'TOOL_INPUT_command',
]);
// ── Source directories to scan ────────────────────────────────────────────────
const SCAN_ROOTS = [
join(REPO_ROOT, 'v3/@claude-flow/cli/src'),
join(REPO_ROOT, 'plugins'),
];
// ── Skip patterns ─────────────────────────────────────────────────────────────
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', 'coverage', '__tests__', 'tests']);
const SCAN_EXTS = new Set(['.ts', '.mjs', '.cjs', '.js']);
// ── Regex to find process.env.CLAUDE_FLOW_* reads ────────────────────────────
// Matches: process.env.CLAUDE_FLOW_FOO or process.env['CLAUDE_FLOW_FOO']
const ENV_READ_RE = /process\.env(?:\.([A-Z_]+)|\[['"]([A-Z_]+)['"]\])/g;
// ── Indicator that a CLI arg takes precedence ─────────────────────────────────
// Presence of any of these in the surrounding 10 lines counts as documented precedence.
const PRECEDENCE_INDICATORS = [
/cli.*flag/i,
/argv/i,
/precedence/i,
/--[a-z]/, // looks like a --flag reference in a comment
/options\.\w+/, // options.someFlag pattern (function param wins)
/args\.\w+/, // args.someFlag pattern
/param.*overrid/i,
/flag.*win/i,
/caller.*can.*pass/i,
];
// ── Walk source tree ─────────────────────────────────────────────────────────
function* walkSourceFiles(dir) {
let entries;
try { entries = readdirSync(dir); } catch { return; }
for (const entry of entries) {
if (SKIP_DIRS.has(entry)) continue;
const full = join(dir, entry);
let st;
try { st = statSync(full); } catch { continue; }
if (st.isDirectory()) {
yield* walkSourceFiles(full);
} else if (st.isFile()) {
const dot = entry.lastIndexOf('.');
if (dot >= 0 && SCAN_EXTS.has(entry.slice(dot))) yield full;
}
}
}
const violations = [];
const warnings = [];
const scanned = [];
for (const root of SCAN_ROOTS) {
if (!existsSync(root)) continue;
for (const file of walkSourceFiles(root)) {
let text;
try { text = readFileSync(file, 'utf8'); } catch { continue; }
const lines = text.split('\n');
scanned.push(relative(REPO_ROOT, file));
let match;
ENV_READ_RE.lastIndex = 0;
while ((match = ENV_READ_RE.exec(text)) !== null) {
const varName = match[1] || match[2];
if (!varName) continue;
if (!varName.startsWith('CLAUDE_FLOW_') && !varName.startsWith('RUFLO_')) continue;
if (KNOWN_ESCAPE_HATCHES.has(varName)) continue;
// Find the line number
const lineIdx = text.slice(0, match.index).split('\n').length - 1;
const contextStart = Math.max(0, lineIdx - 5);
const contextEnd = Math.min(lines.length - 1, lineIdx + 5);
const contextLines = lines.slice(contextStart, contextEnd + 1).join('\n');
// Check for precedence indicators in surrounding context
const hasPrecedenceDoc = PRECEDENCE_INDICATORS.some(re => re.test(contextLines));
const relFile = relative(REPO_ROOT, file);
const lineNo = lineIdx + 1;
if (!hasPrecedenceDoc) {
// Check if it's inside a function with an explicit parameter that could override.
// Heuristic: look for a function declaration within 20 lines above that has params.
const fnContextStart = Math.max(0, lineIdx - 20);
const fnContext = lines.slice(fnContextStart, lineIdx + 1).join('\n');
const hasExplicitParam = /function\s+\w+\s*\([^)]+\)|=>\s*\{|\([^)]+\)\s*:\s*\w+/.test(fnContext)
&& !/function\s+\w+\s*\(\s*\)/.test(fnContext.split('\n').slice(-5).join('\n'));
if (hasExplicitParam) {
// Warn rather than fail — function params could be the override path
warnings.push({ file: relFile, line: lineNo, varName });
} else {
violations.push({
file: relFile,
line: lineNo,
varName,
context: lines[lineIdx]?.trim() ?? '',
});
}
}
}
}
}
// ── Report ────────────────────────────────────────────────────────────────────
console.log(`env-var-precedence audit — scanned ${scanned.length} source file(s)`);
if (warnings.length > 0) {
console.log(`\nwarnings (function-param override path detected — verify manually):`);
for (const w of warnings) {
console.log(` ? ${w.file}:${w.line} ${w.varName}`);
}
}
if (violations.length === 0) {
console.log('\n ok: all CLAUDE_FLOW_* / RUFLO_* env var reads have documented CLI-flag precedence');
console.log(' ok: or are registered as known escape-hatch env vars (CI/test/credential use)');
process.exit(0);
}
console.error(`\n${violations.length} violation(s) — env var read without CLI-flag precedence documentation:`);
for (const v of violations) {
console.error(` x ${v.file}:${v.line} ${v.varName}`);
console.error(` context: ${v.context}`);
}
console.error(`
Remediation:
Option A — Wire a CLI flag that takes precedence:
Before: const val = process.env.CLAUDE_FLOW_FOO;
After: const val = options.foo ?? process.env.CLAUDE_FLOW_FOO ?? DEFAULT;
Then add "// CLI flag options.foo takes precedence over CLAUDE_FLOW_FOO env var"
Option B — Register as an escape hatch (CI/test/credential only):
Add the env var name to KNOWN_ESCAPE_HATCHES in scripts/audit-env-var-precedence.mjs
with a comment explaining why it is intentionally env-only.
Reference: ADR-125 §"CLI flag wins", ADR-130 §env-var-config-precedence.
`);
process.exit(1);