ruvnet/ruflo · error · Error
signBacktestArtifact: privateKey must be 32 bytes
Error message
signBacktestArtifact: privateKey must be 32 bytes (got ${privateKey.length}) What it means
After normalize() and resolve(cwd, ...), validateConfigPath() asserts the resolved path is prefixed by cwd as a containment check ('defense in depth'). Because absolute paths and '..' are already rejected by earlier branches, this branch is hard to reach through normal input; it exists to catch anything that still resolves outside the working directory, most notably Windows drive-relative paths ('C:file.json' normalizes to a form that resolves against the drive root, not cwd).
Solutions
- Drop the drive prefix and pass a plain relative path: 'claude-flow.config.json' instead of 'C:claude-flow.config.json'
- Verify you are not mixing a custom cwd with a path resolved against a different root — pass the path relative to the same cwd the validator uses
- Keep the config file in the same directory tree as the MCP server's working directory
- If you control the call site, prefer omitting 'path' entirely and let the tool default to ./claude-flow.config.json
Example fix
// before (Windows)
await client.callTool('config_save', { path: 'C:claude-flow.config.json', config: cfg }); // drive-relative -> resolves to C:\ ... throws [1123]
// after
await client.callTool('config_save', { path: 'claude-flow.config.json', config: cfg }); Defensive patterns
Strategy: validation
Validate before calling
import { resolve, isAbsolute } from 'path';
function staysWithinCwd(p: string, cwd = process.cwd()): boolean {
if (isAbsolute(p)) return false;
const resolved = resolve(cwd, p);
return resolved === cwd || resolved.startsWith(cwd + require('path').sep);
} Prevention
- On Windows, strip drive-only prefixes ('C:file.json') — always send plain relative paths
- Keep the config file inside the server's working directory tree
- Omit 'path' when the default ./claude-flow.config.json is fine
When it happens
Trigger: On Windows, path="C:claude-flow.config.json" — normalize keeps the drive-relative form, resolve() anchors it to C:\ (or another drive), and the result no longer starts with the cwd; a caller passing a custom cwd argument that differs from where the path actually resolves; exotic UNC/symlinked cwd setups where the resolved prefix diverges.
Common situations: Windows hosts where a config path is copied from Explorer and retains a drive-only prefix; mixed drive setups (cwd on D:, config resolving on C:); code that passes process.cwd() captured at startup while the path was resolved later against a different root.
Related errors
- Dangerous key segment rejected
- unknown game " ". Known
- unknown strategy " ". Available
- flywheel anchor path must stay inside project root
- flywheel anchor symlink escapes project root
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/0869a65ebbedd549.
Report an issue: GitHub.
Appendix: source
Thrown at plugins/ruflo-neural-trader/src/signed-artifact.ts:85
* `SignedBacktestArtifact` envelope.
*
* The signature covers the artifact body WITHOUT `witnessSignature` and
* WITHOUT `witnessPublicKey` (CWE-347 pattern). This means an attacker who
* swaps the served `witnessPublicKey` field cannot bypass verification when
* the verifier pins to a trusted key (which is the only safe verifier).
*
* @param body — the artifact body (everything except signature fields + schema)
* @param privateKeyHex — 32-byte Ed25519 private key as hex string (no 'ed25519:' prefix)
* @returns — the signed artifact ready to be stored
*/
export async function signBacktestArtifact(
body: SignedBacktestArtifactBody,
privateKeyHex: string,
): Promise<SignedBacktestArtifact> {
const ed = await import('@noble/ed25519');
const privateKey = hexToBytes(privateKeyHex);
if (privateKey.length !== 32) {
throw new Error(
`signBacktestArtifact: privateKey must be 32 bytes (got ${privateKey.length})`,
);
}
// Canonical body = the artifact WITHOUT signature fields, plain JSON.stringify.
// Matches scripts/smoke-plugin-registry-signature.mjs:193-200.
const canonical = canonicalBytes(body);
const signatureBytes = await ed.signAsync(canonical, privateKey);
const publicKeyBytes = await ed.getPublicKeyAsync(privateKey);
return {
schema: 'ruflo-neural-trader-backtest/v1',
...body,
witnessPublicKey: `ed25519:${bytesToHex(publicKeyBytes)}`,
witnessSignature: bytesToHex(signatureBytes),
};
}
View on GitHub (pinned to fa13ee4ad6)