{"record":{"id":"b2d8b480bcc7e01f","repo":"ruvnet/ruflo","slug":"signattributionartifact-privatekey-must-be-32-byt","errorCode":null,"errorMessage":"signAttributionArtifact: privateKey must be 32 bytes (got ${privateKey.length})","messagePattern":"signAttributionArtifact: privateKey must be 32 bytes \\(got (.+?)\\)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"plugins/ruflo-neural-trader/src/signed-attribution.ts","lineNumber":101,"sourceCode":" * Sign the body of an attribution artifact and return the fully-formed\n * `SignedAttributionArtifact` envelope.\n *\n * The signature covers the artifact body WITHOUT `witnessSignature` and\n * WITHOUT `witnessPublicKey` (CWE-347 pattern, same as Phase 4). The\n * verifier MUST pin to a trusted key for the pin to be a real defense.\n *\n * @param body                — artifact body (everything except signature fields + schema)\n * @param privateKeyHex       — 32-byte Ed25519 private key as hex (no 'ed25519:' prefix)\n * @returns                     the signed artifact ready to be stored\n */\nexport async function signAttributionArtifact(\n  body: SignedAttributionArtifactBody,\n  privateKeyHex: string,\n): Promise<SignedAttributionArtifact> {\n  const ed = await import('@noble/ed25519');\n  const privateKey = hexToBytes(privateKeyHex);\n  if (privateKey.length !== 32) {\n    throw new Error(\n      `signAttributionArtifact: privateKey must be 32 bytes (got ${privateKey.length})`,\n    );\n  }\n\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-attribution/v1',\n    ...body,\n    witnessPublicKey: `ed25519:${bytesToHex(publicKeyBytes)}`,\n    witnessSignature: bytesToHex(signatureBytes),\n  };\n}\n\n/**\n * Verify a signed attribution artifact against a caller-supplied trusted","sourceCodeStart":83,"sourceCodeEnd":119,"githubUrl":"https://github.com/ruvnet/ruflo/blob/fa13ee4ad60ac2090b1480656eb233521790d640/plugins/ruflo-neural-trader/src/signed-attribution.ts#L83-L119","documentation":"Every session operation (save, load, resume) builds the session file path via getSessionPath(), which first runs validateSessionId(): the ID must match /^[a-zA-Z0-9_-]+$/, be at most 128 chars, and contain no '..', '/', or '\\'. This error is the charset/length gate — it exists because the sessionId becomes a filename under .claude-flow/sessions/, and any other character class would open the door to path manipulation.","triggerScenarios":"sessionId=\"session.123\" (dot not allowed); sessionId with a space or unicode from user input; an empty string; sessionId=\"sess/../../etc/passwd\" or \"back\\\\slash\"; an ID longer than 128 characters; a UUID formatted with braces like \"{550e8400-...}\" — braces fail the regex.","commonSituations":"Passing an email, hostname, or free-form label as the session ID; copy-pasting IDs with trailing whitespace or invisible characters; generating IDs with a library that uses dots (e.g. nanoid custom alphabets, dotted ULIDs); truncation bugs producing 129+ char strings.","solutions":["Sanitize the ID before calling the tool: sessionId.replace(/[^a-zA-Z0-9_-]/g, '_')","Use IDs you generated from the safe alphabet — the tool's own generated IDs (timestamp + base36 random) always pass","Standard UUIDs are fine as-is (hex + hyphens), just strip surrounding braces or quotes","If you need arbitrary metadata attached to a session, keep it inside the session payload, not in the ID"],"exampleFix":"// before\nawait client.callTool('session_resume', { sessionId: 'deploy.2026-08-18 10:00' }); // dots/space -> throws [1125]\n\n// after\nconst sessionId = 'deploy.2026-08-18 10:00'.replace(/[^a-zA-Z0-9_-]/g, '_'); // deploy_2026-08-18_10_00\nawait client.callTool('session_resume', { sessionId });","handlingStrategy":"validation","validationCode":"function isValidSessionId(id: string): boolean {\n  return /^[a-zA-Z0-9_-]+$/.test(id) && id.length <= 128 && !id.includes('..');\n}\nfunction sanitizeSessionId(raw: string): string {\n  return raw.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 128);\n}","typeGuard":"const SAFE_SESSION_ID = /^[a-zA-Z0-9_-]{1,128}$/;\nfunction isSafeSessionId(id: unknown): id is string {\n  return typeof id === 'string' && SAFE_SESSION_ID.test(id) && !id.includes('..');\n}","tryCatchPattern":null,"preventionTips":["Generate session IDs from [a-zA-Z0-9_-] only (UUIDs and the tool's own IDs qualify)","Run sanitizeSessionId() on any user-supplied ID before it reaches a session tool","Attach free-form metadata to the session payload, never to the ID string"],"tags":["mcp","session","input-validation","security"],"backgroundTag":"invalid-session-id","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"}