ruvnet/ruflo · error · Error

signAttributionArtifact: privateKey must be 32 bytes

Error message

signAttributionArtifact: privateKey must be 32 bytes (got ${privateKey.length})

What it means

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.

Solutions

  1. Sanitize the ID before calling the tool: sessionId.replace(/[^a-zA-Z0-9_-]/g, '_')
  2. Use IDs you generated from the safe alphabet — the tool's own generated IDs (timestamp + base36 random) always pass
  3. Standard UUIDs are fine as-is (hex + hyphens), just strip surrounding braces or quotes
  4. If you need arbitrary metadata attached to a session, keep it inside the session payload, not in the ID

Example fix

// before
await client.callTool('session_resume', { sessionId: 'deploy.2026-08-18 10:00' }); // dots/space -> throws [1125]

// after
const sessionId = 'deploy.2026-08-18 10:00'.replace(/[^a-zA-Z0-9_-]/g, '_'); // deploy_2026-08-18_10_00
await client.callTool('session_resume', { sessionId });
Defensive patterns

Strategy: validation

Validate before calling

function isValidSessionId(id: string): boolean {
  return /^[a-zA-Z0-9_-]+$/.test(id) && id.length <= 128 && !id.includes('..');
}
function sanitizeSessionId(raw: string): string {
  return raw.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 128);
}

Type guard

const SAFE_SESSION_ID = /^[a-zA-Z0-9_-]{1,128}$/;
function isSafeSessionId(id: unknown): id is string {
  return typeof id === 'string' && SAFE_SESSION_ID.test(id) && !id.includes('..');
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18). Data as JSON: /api/errors/b2d8b480bcc7e01f. Report an issue: GitHub.

Appendix: source

Thrown at plugins/ruflo-neural-trader/src/signed-attribution.ts:101

 * Sign the body of an attribution artifact and return the fully-formed
 * `SignedAttributionArtifact` envelope.
 *
 * The signature covers the artifact body WITHOUT `witnessSignature` and
 * WITHOUT `witnessPublicKey` (CWE-347 pattern, same as Phase 4). The
 * verifier MUST pin to a trusted key for the pin to be a real defense.
 *
 * @param body                — artifact body (everything except signature fields + schema)
 * @param privateKeyHex       — 32-byte Ed25519 private key as hex (no 'ed25519:' prefix)
 * @returns                     the signed artifact ready to be stored
 */
export async function signAttributionArtifact(
  body: SignedAttributionArtifactBody,
  privateKeyHex: string,
): Promise<SignedAttributionArtifact> {
  const ed = await import('@noble/ed25519');
  const privateKey = hexToBytes(privateKeyHex);
  if (privateKey.length !== 32) {
    throw new Error(
      `signAttributionArtifact: privateKey must be 32 bytes (got ${privateKey.length})`,
    );
  }

  const canonical = canonicalBytes(body);
  const signatureBytes = await ed.signAsync(canonical, privateKey);
  const publicKeyBytes = await ed.getPublicKeyAsync(privateKey);

  return {
    schema: 'ruflo-neural-trader-attribution/v1',
    ...body,
    witnessPublicKey: `ed25519:${bytesToHex(publicKeyBytes)}`,
    witnessSignature: bytesToHex(signatureBytes),
  };
}

/**
 * Verify a signed attribution artifact against a caller-supplied trusted

View on GitHub (pinned to fa13ee4ad6)