ruvnet / ruvnet/ruflo

SONA env validation accepts inherited object keys as learning modes

Open Beginner friendly
#3,345 0 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.