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

  1. Drop the drive prefix and pass a plain relative path: 'claude-flow.config.json' instead of 'C:claude-flow.config.json'
  2. 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
  3. Keep the config file in the same directory tree as the MCP server's working directory
  4. 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

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


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)