ruvnet/ruflo · error · Error

unknown strategy " ". Available

Error message

unknown strategy "${name}". Available: ${roster.map((r) => r.name).join(', ')}

What it means

validateConfigPath() calls normalize() on the input and then rejects any result containing the substring '..'. The intent is to block classic path traversal (../../etc/passwd.json) so a config path can never climb above the server's working directory. Note the check is substring-based, so even a harmless filename containing two dots such as "my..config.json" is rejected, not just real traversal segments.

Solutions

  1. Remove all '..' segments: pass the direct relative path, or pre-compute path.relative(process.cwd(), targetPath) which yields a clean traversal-free path
  2. If the target genuinely sits above the server's cwd, restart the MCP server from that parent directory so a flat relative path works
  3. Rename files whose names contain '..' (e.g. my..config.json -> my.config.json) since the substring check cannot distinguish them from traversal
  4. Double-check the path string for accidental '..' introduced by template literals or string concatenation

Example fix

// before
await client.callTool('config_save', {
  path: '../shared/claude-flow.config.json', // throws [1121]
  config: cfg,
});

// after (run server from repo root, or flatten the path)
await client.callTool('config_save', {
  path: 'shared/claude-flow.config.json',
  config: cfg,
});
Defensive patterns

Strategy: validation

Validate before calling

import { relative, isAbsolute, sep } from 'path';

function safeRelativeConfigPath(raw: string): string | null {
  const rel = isAbsolute(raw) ? relative(process.cwd(), raw) : raw;
  const norm = rel.replace(/\\/g, '/');
  if (norm.split('/').some(seg => seg === '..' || seg.includes('..'))) return null;
  return norm;
}

Try / catch

try { await client.callTool('config_load', { path }); }
catch (e) {
  if (e instanceof Error && e.message.includes('Path traversal')) {
    throw new Error(`Config path ${path} escapes the server cwd; move the file or change server cwd`);
  }
  throw e;
}

Prevention

When it happens

Trigger: path="../../../etc/app/config.json"; path="configs/../claude-flow.config.json" (normalize() keeps the '..' because it would climb, so it survives); a benign filename with a double dot like "v2..release.config.json" which triggers a false positive on the includes('..') test.

Common situations: Reusing CLI flags or templates that include ../ to point at a parent-directory config; generated filenames containing '..' (version strings like '1.0..json'); user input sanitized for traversal elsewhere but not for dot-runs.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18). Data as JSON: /api/errors/8df4598af09389ff. Report an issue: GitHub.

Appendix: source

Thrown at plugins/ruflo-arena/src/domain/strategies.ts:130

      antiCopy(a, 0, 'suspicious-anti-tft'),
      alternate(a, 'alternate'),
      random(a, 'random'),
    ];
  }
  return [
    constant(a, 0, `always-${a[0]}`),
    constant(a, 1, `always-${a[1]}`),
    copyOpponent(a, 0, 'copy-opponent'),
    antiCopy(a, 0, 'anti-copy'),
    alternate(a, 'alternate'),
    random(a, 'random'),
  ];
}

export function findStrategy(game: GameSpec, name: string): Strategy {
  const roster = classicRoster(game);
  const s = roster.find((r) => r.name === name || r.name.startsWith(name));
  if (!s) throw new Error(`unknown strategy "${name}". Available: ${roster.map((r) => r.name).join(', ')}`);
  return s;
}

// --- Evolvable FSMs — random genomes + mutation operators (ADR-148) ------------------------

export function randomFSM(game: GameSpec, rng: () => number, nStates = 2, name = 'evolved'): FsmStrategy {
  const a = game.actions;
  const states = [];
  for (let i = 0; i < nStates; i++) {
    states.push({
      action: choice(rng, a),
      next: Object.fromEntries(a.map((x) => [x, randInt(rng, nStates)])),
    });
  }
  return { kind: 'fsm', name, nStates, start: randInt(rng, nStates), states };
}

function cloneFSM(fsm: FsmStrategy): FsmStrategy {

View on GitHub (pinned to fa13ee4ad6)