SONA env validation accepts inherited object keys as learning modes
- Dominant language
- TypeScript
- Stars
- 72.7k
- Forks
- 8.6k
- Avg merge
- 2d 23h
- Merged PRs (30d)
- 83
Description
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](https://github.com/ruvnet/ruflo/issues/3335#issuecomment-5691416170) 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()](https://github.com/ruvnet/ruflo/blob/5e634b396511420d254637b1af6d57290281d283/v3/@claude-flow/integration/src/sona-adapter.ts#L108) 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()](https://github.com/ruvnet/ruflo/blob/5e634b396511420d254637b1af6d57290281d283/v3/@claude-flow/integration/src/sona-adapter.ts#L631).
[applyModeConfig()](https://github.com/ruvnet/ruflo/blob/5e634b396511420d254637b1af6d57290281d283/v3/@claude-flow/integration/src/sona-adapter.ts#L656) 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](https://github.com/ruvnet/ruflo/blob/5e634b396511420d254637b1af6d57290281d283/v3/@claude-flow/memory/src/learning-bridge.ts#L93) 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**:
```text
# 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:
```text
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:
```sh
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.
```js
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 ?? ''}`, 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](https://github.com/ruvnet/ruflo/blob/5e634b396511420d254637b1af6d57290281d283/v3/@claude-flow/integration/src/__tests__/sona-adapter.test.ts#L40) 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.
Contributor guide
Research direction
Start with sonaModeFromEnv(), mergeConfig(), and applyModeConfig() in v3/@claude-flow/integration/src/sona-adapter.ts, then read the existing cases in v3/@claude-flow/integration/src/__tests__/sona-adapter.test.ts. Run the supplied Node regression test and the Integration tests. Done means inherited keys such as constructor, toString, and __proto__ fall back like other invalid values while all listed legitimate modes and explicit mode precedence remain covered.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- node.js, typescript
- Domain
- backend, testing
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 86/100