ruvnet/ruflo · error
RUFLO_X_ADMIN_TOKEN is not set (gateway-identity writes are…
Error message
RUFLO_X_ADMIN_TOKEN is not set (gateway-identity writes are admin-gated)
What it means
Thrown by the `x_federation_publish` tool handler when `adminToken()` returns no value because the `RUFLO_X_ADMIN_TOKEN` environment variable is unset. Gateway-identity writes (publishing as the gateway identity rather than a node key) are admin-gated, so the tool refuses to run before making any network call. It is a deliberate guardrail, not an infra failure.
Solutions
- Export RUFLO_X_ADMIN_TOKEN in the environment that launches the MCP/CLI process, then retry: `export RUFLO_X_ADMIN_TOKEN=<token>`.
- If using a .env file, ensure it is actually loaded by the process and contains the key.
- In CI, add the secret to the pipeline's environment for the job step.
- If you are a regular node (not the gateway operator), do not use x_federation_publish — join with your own key via invite→claim and publish yourself, as the tool description advises.
- Pre-flight the variable before calling: `if (!process.env.RUFLO_X_ADMIN_TOKEN) throw ...`.
Example fix
// before: calling publish without the admin token in env
await xFederationPublish({ msgType: 'Status', payload });
// after: check and fail fast with guidance
if (!process.env.RUFLO_X_ADMIN_TOKEN) throw new Error('set RUFLO_X_ADMIN_TOKEN or publish with your own node key');
await xFederationPublish({ msgType: 'Status', payload }); Defensive patterns
Strategy: validation
Validate before calling
function requireAdminToken(): string {
const t = process.env.RUFLO_X_ADMIN_TOKEN;
if (!t || t.trim() === '') throw new Error('RUFLO_X_ADMIN_TOKEN is not set; gateway-identity publish is admin-gated');
return t;
} Type guard
function hasAdminToken(env: NodeJS.ProcessEnv): env is NodeJS.ProcessEnv & { RUFLO_X_ADMIN_TOKEN: string } {
return typeof env.RUFLO_X_ADMIN_TOKEN === 'string' && env.RUFLO_X_ADMIN_TOKEN.length > 0;
} Try / catch
try {
await xFederationPublish({ msgType, payload });
} catch (e) {
if (e instanceof Error && e.message.includes('RUFLO_X_ADMIN_TOKEN is not set')) {
console.error('export RUFLO_X_ADMIN_TOKEN in the MCP server's environment, or publish with your own node key');
} else throw e;
} Prevention
- Export RUFLO_X_ADMIN_TOKEN before launching the CLI/MCP process — env is read at process start.
- In CI, declare the token as a pipeline secret and inject it into the job step.
- Add a startup check that fails fast when admin-gated tools are expected to be used.
- Keep operator-only tools (publish/invite_mint) on the gateway operator machine; nodes use their own keys.
- Never commit tokens to the repo; load from a secrets manager or untracked .env.
When it happens
Trigger: Invoking `x_federation_publish` (MCP tool) in any environment where RUFLO_X_ADMIN_TOKEN is not exported: CI runners without the secret, fresh shells that never sourced the env file, MCP server processes started without inheriting the variable, or operators running unprivileged node setups where the token was intentionally withheld.
Common situations: Forgetting to `export RUFLO_X_ADMIN_TOKEN=...` before starting the MCP server (env must be present at process start); a .env file not loaded by the CLI; CI/CD secrets not passed to the step; attempting a hub-level broadcast from a node machine that legitimately has no admin token.
Understand the failure class
Background: "environment variable is not set" and "Missing keys in environment" errors: what missing required env var messages mean and how to fix them — this error's family across 28 libraries.
Related errors
- RUFLO_X_ADMIN_TOKEN is not set (invite minting is…
- agents must have ≥1 entry
- allowedMcpTools entries must be non-empty strings
- allowedMcpTools must have ≥1 entry
- ArtifactLedger requires an explicit signingKey — hardcoded…
AI-assisted analysis of ruvnet/ruflo@9c61c86f06 (2026-09-15).
Data as JSON: /api/errors/f8e3cfb3919f4ee9.
Report an issue: GitHub.
Appendix: source
Thrown at v3/@claude-flow/cli/src/mcp-tools/x-federation-tools.ts:167
description:
'Return the current owner-per-resource work-claims ledger for the open swarm (ruv://claims/board). Use when you are about to start shared work and need to know whether a resourceId is already owned. Inferring ownership from raw ClaimIssued events is wrong because releases, TTL expiry and handoffs change the answer; the board applies those rules.',
inputSchema: { type: 'object', properties: { ...gatewayArg } },
handler: async (input) => gatewayResource('ruv://claims/board', (input as Record<string, unknown>).gatewayUrl),
},
{
name: 'x_federation_registry',
description:
'Read the federation registry resource (ruv://federation/registry): relay URL, canonical relay tag for NIP-42, gateway pubkey, and the exact self-join steps. Use when onboarding a new node or user to the open federation. Hard-coding the relay URL is wrong because the relay verifies the NIP-42 relay tag strictly against its canonical host, which this resource states.',
inputSchema: { type: 'object', properties: { ...gatewayArg } },
handler: async (input) => gatewayResource('ruv://federation/registry', (input as Record<string, unknown>).gatewayUrl),
},
{
name: 'x_federation_publish',
description:
'Publish a signed coordination message to the open swarm AS THE GATEWAY identity (Status/Task/Result/…). Requires RUFLO_X_ADMIN_TOKEN. Use when a trusted operator needs a hub-level broadcast. Using this to post on behalf of an individual node is wrong because it attributes the message to the gateway, not the node — nodes should join with their own key via invite→claim and publish themselves.',
inputSchema: { type: 'object', properties: { ...gatewayArg, msgType: { type: 'string' }, payload: { type: 'object' } }, required: ['msgType', 'payload'] },
handler: async (input) => {
const t = adminToken(); if (!t) throw new Error('RUFLO_X_ADMIN_TOKEN is not set (gateway-identity writes are admin-gated)');
return gatewayTool('federation_publish', { ...(input as Record<string, unknown>), adminToken: t });
},
},
{
name: 'x_federation_invite_mint',
description:
'Mint a use-limited, expiring invite code so a new ruflo user can self-join the open federation with THEIR OWN key. Requires RUFLO_X_ADMIN_TOKEN. Use when onboarding someone. Sharing the relay owner key instead is wrong because invites are revocable, hashed at rest, and bind membership to the claimant\'s key; the code is a bearer secret — hand it over privately.',
inputSchema: { type: 'object', properties: { ...gatewayArg, ttlSecs: { type: 'number', description: 'Validity (default 7 days).' }, maxUses: { type: 'number', description: 'Redemptions (default 25).' } } },
handler: async (input) => {
const t = adminToken(); if (!t) throw new Error('RUFLO_X_ADMIN_TOKEN is not set (invite minting is admin-gated)');
return gatewayTool('federation_invite_mint', { ...(input as Record<string, unknown>), adminToken: t });
},
},
{
name: 'x_federation_admit',
description:
'Admit a Nostr pubkey as a relay member directly (NIP-43 kind 9030). Requires RUFLO_X_ADMIN_TOKEN. Use when a known node reports its 64-hex pubkey and you want to skip the invite step. Padding or hand-editing a reported pubkey is wrong because it is a cryptographic identity; a malformed key must be re-reported, never fixed up.',
inputSchema: { type: 'object', properties: { ...gatewayArg, pubkey: { type: 'string', description: '64-hex secp256k1 x-only pubkey.' }, role: { type: 'string', enum: ['member', 'admin'] } }, required: ['pubkey'] },View on GitHub (pinned to 9c61c86f06)