ruvnet/ruflo · error
channel must be pub: or prv:<16 hex>
Error message
channel must be pub:<name> or prv:<16 hex>
What it means
The federation publish tool validates the channel identifier against CHANNEL_ID_RE, which only accepts public channels of the form pub:<name> or private channels of the form prv:<16 hex chars>. Any other string (bare name, wrong prefix, wrong length/charset for private) is rejected before any network work.
Solutions
- Format public channels as pub:<name>, e.g. pub:general
- Format private channels as exactly prv: plus 16 lowercase/uppercase hex characters, copying the id returned when the channel was created
- Copy channel ids directly from the create/grant tool output instead of typing them
Example fix
// before
await publish({ channel: 'general', msgType: 'Status', payload: {} });
// after
await publish({ channel: 'pub:general', msgType: 'Status', payload: {} }); Defensive patterns
Strategy: validation
Validate before calling
const CHANNEL_ID_RE = /^(pub:[^\s]+|prv:[0-9a-fA-F]{16})$/;
if (!CHANNEL_ID_RE.test(channel)) throw new Error(`bad channel id: ${channel}`); Type guard
const isValidChannelId = (ch: unknown): ch is string =>
typeof ch === 'string' && /^(pub:[^\s]+|prv:[0-9a-fA-F]{16})$/.test(ch); Try / catch
try {
await publish({ channel, msgType, payload });
} catch (e) {
if (String(e.message).startsWith('channel must be pub:')) {
console.error(`Channel '${channel}' is malformed; expected pub:<name> or prv:<16 hex>`);
} else throw e;
} Prevention
- Always include the pub:/prv: prefix on channel ids
- Copy private channel ids (16 hex) from tool output instead of typing them
- Validate channel strings at config load time before making calls
When it happens
Trigger: Passing a channel like 'general' (no prefix), 'private:abc' (wrong prefix), 'prv:xyz' (fewer than 16 hex chars), or 'prv:zzzz...' (non-hex characters) to the publish tool.
Common situations: Users echoing a channel name from logs without the pub:/prv: prefix; hand-crafting a private channel id that is not exactly 16 hex characters; confusing this CLI's channel format with another product's channel naming.
Understand the failure class
Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.
Related errors
- invite code must look like v2.
- msgType must be alnum + _ - and ≤64 chars
- nodeId must be 16 lowercase hex chars
- actualUsd must be a non-negative finite number
- Agent config must include id, name, and type
AI-assisted analysis of ruvnet/ruflo@2602b642d9 (2026-09-15).
Data as JSON: /api/errors/0aa1d03967cbf4bb.
Report an issue: GitHub.
Appendix: source
Thrown at v3/@claude-flow/cli/src/mcp-tools/x-federation-channels.ts:195
accepted.push(body.channel);
} catch { failed.push(body.channel); }
}
if (accepted.length) writeStore(store);
return { pubkey, accepted: [...new Set(accepted)], unopenable: [...new Set(failed)], keyStoredAt: STORE_FILE() };
},
},
{
name: 'x_federation_channel_publish',
description: 'Publish a message to a channel with YOUR OWN key. A private channel is encrypted locally under its channel key before it leaves this machine, and the message type is hidden behind k=enc so the relay sees only an opaque id and ciphertext. Use when the message should be attributable to you. The admin-gated gateway channel_publish is wrong for that, because it signs as the gateway and cannot reach private channels at all.',
inputSchema: { type: 'object', properties: {
channel: { type: 'string', description: 'Channel id (pub:<name> or prv:<16 hex>).' },
msgType: { type: 'string', description: 'Message type (Status, Task, Result, …). Hidden on private channels.' },
payload: { type: 'object', description: 'JSON body. Never put secrets or credentials in it, even on a private channel.' },
relayWs: { type: 'string', description: 'Relay URL; takes precedence over RUFLO_X_RELAY_WS.' },
}, required: ['channel', 'msgType', 'payload'] },
handler: async (input) => {
const i = input as { channel: string; msgType: string; payload: Record<string, unknown>; relayWs?: string };
if (!CHANNEL_ID_RE.test(i.channel)) throw new Error('channel must be pub:<name> or prv:<16 hex>');
const t = await loadTools(); if (!t) return degraded();
const { sk, pubkey } = loadOrCreateKey(t.nt as never, KEY_FILE());
const priv = isPrivateChannel(i.channel);
const body = { type: i.msgType, from: pubkey, ts: new Date().toISOString(), ...i.payload };
let content: string;
if (priv) {
const entry = readStore()[i.channel];
if (!entry) throw new Error(`no key held for ${i.channel} — accept a grant first (x_federation_channel_accept)`);
content = t.nip44.v2.encrypt(JSON.stringify(body), Uint8Array.from(Buffer.from(entry.key, 'hex')));
} else { content = JSON.stringify(body); }
const tags = [['t', 'ruflo-swarm'], ['c', i.channel], ['k', priv ? 'enc' : i.msgType]];
const eventId = await relayCall(RELAY_WS(i.relayWs), sk, t.nt, (ws) => publishEvent(ws, t.nt, sk, tags, content));
return { ok: true, channel: i.channel, visibility: priv ? 'private' : 'public', encrypted: priv, eventId, pubkey };
},
},
{
name: 'x_federation_channel_read',
description: 'Read a channel and decrypt what your keys can open. Public messages come back as JSON; private ones are decrypted locally with the cached channel key, and anything you have no key for is returned as encrypted:true rather than silently dropped. Use when the channel is private: reading it through the gateway channel_sync tool is wrong there, because the gateway holds no key and can only hand you ciphertext.',View on GitHub (pinned to 2602b642d9)