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
- Remove all '..' segments: pass the direct relative path, or pre-compute path.relative(process.cwd(), targetPath) which yields a clean traversal-free path
- If the target genuinely sits above the server's cwd, restart the MCP server from that parent directory so a flat relative path works
- Rename files whose names contain '..' (e.g. my..config.json -> my.config.json) since the substring check cannot distinguish them from traversal
- 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
- Never build config paths with '../' — compute path.relative(cwd, target) once and cache it
- Add a lint rule or unit test asserting your config path constants contain no '..' substring
- Keep shared configs inside the server's working tree instead of reaching up to parent directories
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
- Dangerous key segment rejected
- localSingleEntryPageRank: sourceIndex
- memory path contains disallowed characters
- namespace contains path traversal
- signBacktestArtifact: privateKey must be 32 bytes
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)