{"record":{"id":"0869a65ebbedd549","repo":"ruvnet/ruflo","slug":"signbacktestartifact-privatekey-must-be-32-bytes","errorCode":null,"errorMessage":"signBacktestArtifact: privateKey must be 32 bytes (got ${privateKey.length})","messagePattern":"signBacktestArtifact: privateKey must be 32 bytes \\(got (.+?)\\)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"plugins/ruflo-neural-trader/src/signed-artifact.ts","lineNumber":85,"sourceCode":" * `SignedBacktestArtifact` envelope.\n *\n * The signature covers the artifact body WITHOUT `witnessSignature` and\n * WITHOUT `witnessPublicKey` (CWE-347 pattern). This means an attacker who\n * swaps the served `witnessPublicKey` field cannot bypass verification when\n * the verifier pins to a trusted key (which is the only safe verifier).\n *\n * @param body                 — the artifact body (everything except signature fields + schema)\n * @param privateKeyHex        — 32-byte Ed25519 private key as hex string (no 'ed25519:' prefix)\n * @returns                      — the signed artifact ready to be stored\n */\nexport async function signBacktestArtifact(\n  body: SignedBacktestArtifactBody,\n  privateKeyHex: string,\n): Promise<SignedBacktestArtifact> {\n  const ed = await import('@noble/ed25519');\n  const privateKey = hexToBytes(privateKeyHex);\n  if (privateKey.length !== 32) {\n    throw new Error(\n      `signBacktestArtifact: privateKey must be 32 bytes (got ${privateKey.length})`,\n    );\n  }\n\n  // Canonical body = the artifact WITHOUT signature fields, plain JSON.stringify.\n  // Matches scripts/smoke-plugin-registry-signature.mjs:193-200.\n  const canonical = canonicalBytes(body);\n  const signatureBytes = await ed.signAsync(canonical, privateKey);\n  const publicKeyBytes = await ed.getPublicKeyAsync(privateKey);\n\n  return {\n    schema: 'ruflo-neural-trader-backtest/v1',\n    ...body,\n    witnessPublicKey: `ed25519:${bytesToHex(publicKeyBytes)}`,\n    witnessSignature: bytesToHex(signatureBytes),\n  };\n}\n","sourceCodeStart":67,"sourceCodeEnd":103,"githubUrl":"https://github.com/ruvnet/ruflo/blob/fa13ee4ad60ac2090b1480656eb233521790d640/plugins/ruflo-neural-trader/src/signed-artifact.ts#L67-L103","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before (Windows)\nawait client.callTool('config_save', { path: 'C:claude-flow.config.json', config: cfg }); // drive-relative -> resolves to C:\\ ... throws [1123]\n\n// after\nawait client.callTool('config_save', { path: 'claude-flow.config.json', config: cfg });","handlingStrategy":"validation","validationCode":"import { resolve, isAbsolute } from 'path';\nfunction staysWithinCwd(p: string, cwd = process.cwd()): boolean {\n  if (isAbsolute(p)) return false;\n  const resolved = resolve(cwd, p);\n  return resolved === cwd || resolved.startsWith(cwd + require('path').sep);\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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"],"tags":["mcp","config","path-containment","security","windows"],"backgroundTag":"path-outside-base-directory","analyzedSha":"fa13ee4ad60ac2090b1480656eb233521790d640","analyzedAt":"2026-08-18T21:34:22.708Z","contentChangedAt":"2026-08-18T21:34:22.708Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}