SONA env validation accepts inherited object keys as learning modes
I found a validation edge case in the SONA env default added in #3334. RUFLO_INTELLIGENCE_MODE=constructor is accepted by the Integration parser and survives SONAAdapter.initialize(), while Memory treats the same value as unknown and falls back to balanced. toString and __proto__ behave the same way.
This is scoped to direct use of the Integration adapter. I saw the note in #3335 about Integration not being wired into the published CLI, so I'm not reporting this as a CLI-wide runtime failure. It's also separate from the Memory constructor-argument fix in #3336.
Local reproduction
Tested against a clean worktree at 5e634b396511420d254637b1af6d57290281d283, using Node v24.12.0 on Windows x64. The tests import the actual source modules using Node's TypeScript support; no package build or dependency install was needed. A fresh fetch before filing reached cdc000fb418c1fedc6fa9b831c374342badc704e; both relevant production files are unchanged from the tested commit.
Expected: only real-time, balanced, research, edge, and batch should be accepted. Any other env value should follow the existing balanced fallback.
Observed:
| Env value | Integration helper | Adapter before and after initialize | Memory getSonaMode() |
|---|---|---|---|
| unset | undefined |
balanced |
balanced |
balanced |
balanced |
balanced |
balanced |
research |
research |
research |
research |
edge |
edge |
edge |
edge |
garbage / not-a-real-mode |
undefined |
balanced |
balanced |
constructor |
constructor |
constructor |
balanced |
toString |
toString |
toString |
balanced |
__proto__ |
__proto__ |
__proto__ |
balanced |
hasOwnProperty |
hasOwnProperty |
hasOwnProperty |
balanced |
valueOf |
valueOf |
valueOf |
balanced |
The adapter's initialized event and getStats().currentMode also retain the invalid value. All five legitimate modes passed, and an explicit mode: 'edge' still takes precedence over an invalid env value.
Where it happens
sonaModeFromEnv() checks raw in MODE_CONFIGS. The configuration table is a plain object, so that also accepts inherited properties. The returned string then goes straight into mergeConfig().
applyModeConfig() doesn't reject it later: MODE_CONFIGS['constructor'] is a truthy function, and copying its enumerable own properties applies no profile settings. For __proto__, the lookup yields Object.prototype with the same no-op result in an ordinary, unmodified runtime. Memory's explicit whitelist already rejects these strings.
What changes at runtime
With new SONAAdapter() and no custom settings, initialization completes and the numeric defaults happen to match balanced. Pattern storage, search, and a learning cycle continued to work in the local probe. The confirmed problem there is the invalid mode and skipped profile application, not a crash or an undefined configuration.
There is a behavioral difference when custom settings are supplied without an explicit mode. With the same new SONAAdapter({ similarityThreshold: 0.5 }), I stored the pattern one two three four five six seven eight nine ten and searched for one two three four five six (Jaccard similarity 0.6):
| Env value | Threshold after initialization | findSimilarPatterns() results |
|---|---|---|
garbage |
0.7 | 0 |
constructor |
0.5 | 1 |
toString |
0.5 | 1 |
__proto__ |
0.5 | 1 |
The ordinary invalid value falls back to balanced and applies its profile; inherited keys leave the supplied threshold in place. This was observed through the real public search method, without changing the adapter's internal state.
Regression evidence
The local regression ran twice in separate processes against unchanged production source. Both runs returned exit code 1:
# tests 20
# pass 11
# fail 9
# cancelled 0
# skipped 0
The nine failures are the helper, constructor-state, and initialized-state assertions for each of constructor, toString, and __proto__. The three Memory checks and eight control cases pass.
Core assertion output:
helper:
+ actual - expected
+ 'constructor'
- undefined
adapter, before and after initialize:
+ actual - expected
+ 'constructor'
- 'balanced'
code: ERR_ASSERTION
Re-runnable regression (Node 24, no dependencies)Save this as sona-env.regression.test.mjs at the checkout root, then run:
node --test --test-reporter=tap ./sona-env.regression.test.mjs
This is the local test with only the import paths adjusted for the checkout root. The Memory backend is a fail-on-access placeholder: construction, the getter, and cleanup don't use storage. It does not replace either parser or class, and the test does not invoke Memory's lazy neural loader.
import { test, afterEach } from 'node:test';
import assert from 'node:assert/strict';
import { SONAAdapter, sonaModeFromEnv } from './v3/@claude-flow/integration/src/sona-adapter.ts';
import { LearningBridge } from './v3/@claude-flow/memory/src/learning-bridge.ts';
const key = 'RUFLO_INTELLIGENCE_MODE';
const original = process.env[key];
afterEach(() => { if (original === undefined) delete process.env[key]; else process.env[key] = original; });
const backend = new Proxy({}, { get(_target, property) { throw new Error(`Unexpected backend access: ${String(property)}`); } });
for (const value of ['constructor', 'toString', '__proto__']) {
test(`helper rejects inherited key ${value}`, () => {
process.env[key] = value;
assert.equal(sonaModeFromEnv(), undefined);
});
test(`adapter falls back to balanced for ${value}`, async () => {
process.env[key] = value;
const adapter = new SONAAdapter();
try { assert.equal(adapter.getMode(), 'balanced'); }
finally { await adapter.shutdown(); }
});
test(`initialized adapter falls back to balanced for ${value}`, async () => {
process.env[key] = value;
const adapter = new SONAAdapter();
try {
await adapter.initialize();
assert.equal(adapter.getMode(), 'balanced');
} finally { await adapter.shutdown(); }
});
test(`memory rejects inherited key ${value}`, () => {
process.env[key] = value;
const bridge = new LearningBridge(backend);
try { assert.equal(bridge.getSonaMode(), 'balanced'); }
finally { bridge.destroy(); }
});
}
for (const value of [undefined, 'real-time', 'balanced', 'research', 'edge', 'batch', 'garbage', 'not-a-real-mode']) {
test(`control ${value ?? '<unset>'}`, async () => {
if (value === undefined) delete process.env[key]; else process.env[key] = value;
const valid = ['real-time', 'balanced', 'research', 'edge', 'batch'].includes(value);
const expected = valid ? value : 'balanced';
assert.equal(sonaModeFromEnv(), valid ? value : undefined);
const adapter = new SONAAdapter();
const bridge = new LearningBridge(backend);
try {
assert.equal(adapter.getMode(), expected);
await adapter.initialize();
assert.equal(adapter.getMode(), expected);
assert.equal(bridge.getSonaMode(), expected);
} finally { await adapter.shutdown(); bridge.destroy(); }
});
}
The existing Integration tests cover not-a-real-mode, but no inherited keys or post-initialization profile behavior. That's the missing case: an ordinary unknown string never exercises prototype-chain membership.
I'd keep this scoped to the Integration env validator and regression coverage. Memory already has the intended behavior, and the normal outer AgenticFlowBridge path supplies an explicit balanced default, which masks this env path. Nothing in this reproduction depends on modifying a prototype.
Source: ruvnet/ruflo