SONA env validation accepts inherited object keys as learning modes

Open Beginner friendly
#3,345 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
86/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
node.js, typescript
Domain
backend, testing

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.

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 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.

Dominant language
TypeScript
Stars
72.9k
Forks
8.7k
Avg merge
3d 10h
Merged PRs (30d)
74

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from ruvnet/ruflo

All issues in ruvnet/ruflo

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.