mirror of
https://github.com/ruvnet/ruflo.git
synced 2026-09-14 14:01:28 +08:00
dbf450a927
* feat(federation): ruflo CLI + MCP integration for x.ruv.io, and Seraphina (swarm queen)
Integrate the open swarm federation into ruflo itself, and add Seraphina —
a primary-coordinator / swarm-queen guidance tool in the style of the ruOS
assistant terminal (invoked from a terminal or any MCP client).
MCP tools (src/mcp-tools/x-federation-tools.ts), registered in the barrel and
the in-process registry so `callMCPTool` and the MCP server both see them:
x_federation_sync / roster / claims / registry (open reads)
x_federation_publish / invite_mint / admit (gateway-identity writes,
require RUFLO_X_ADMIN_TOKEN; fail closed with no network call)
Each description follows ADR-112 ("Use when … wrong because …").
Seraphina (src/mcp-tools/seraphina-tools.ts): `seraphina_guidance { goal }`
gathers the live roster, claims board and recent messages from the x.ruv.io
gateway, compacts them (dedupe by from|type, cap 15 — a cheap tier drowns in
repeated PeerHellos), and asks the cognitum meta-llm gateway
(https://api.cognitum.one/v1/messages, model cognitum-auto by default with a
tier override) using a queen system prompt that treats message content as
data, respects one-owner-per-resource claims, and returns
{ guidance, proposals[], risks[] }. Proposals are advisory. JSON is extracted
by slicing the outermost object so fenced/prose-wrapped answers still parse.
Key from SERAPHINA_METALLM_KEY (GCP secret seraphina-metallm-api-key).
CLI: `ruflo federation sync|roster|claims|registry|invite|admit|publish`
(src/commands/federation.ts), thin over the same tools.
Skill: .agents/skills/open-federation/SKILL.md documents CLI, tools,
Seraphina, the claims rules, the canonical NIP-42 relay-tag gotcha, and
onboarding.
Tests: 11 (x-federation: RPC shaping, SSE parse, resource mapping, fail-closed
admin gating, isError surfacing; seraphina: registration, fail-closed key,
context gathering + queen prompt + x-api-key, tier override, fenced-JSON
extraction). Project typechecks at 0 errors.
Verified live: Seraphina against the real 3-node federation returned 3
structured proposals + 4 risks via cognitum-auto (routed to a cheap tier).
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_013u4pmL9ZUAXb6usVQgNo67
* fix(federation): ADR-125 env-var precedence — URL config takes a flag/arg, credentials registered env-only
The env-var-precedence audit correctly flagged the new process.env reads.
Config URLs now have a precedence path: `ruflo federation --gateway` feeds a
`gatewayUrl` tool arg (and Seraphina takes `metaLlmUrl`/`gatewayUrl`) that
wins over RUFLO_X_GATEWAY_URL / SERAPHINA_METALLM_URL, documented per ADR-125.
Credentials (RUFLO_X_ADMIN_TOKEN, SERAPHINA_METALLM_KEY) are registered as
env-only escape hatches with rationale: a secret must never be a CLI flag.
+1 test (arg precedence, arg not forwarded upstream). Audit passes locally.
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_013u4pmL9ZUAXb6usVQgNo67
* feat(federation): `ruflo federation join --code` — self-service invite access with your own key
The user-facing path into the open swarm. Decentralized by design: the user
generates/holds THEIR OWN Nostr key (~/.ruflo/nostr.key, 0600), redeems an
invite code with a NIP-98-signed claim directly against the relay (no admin in
the loop), proves membership via NIP-42, and can then publish as themselves.
The gateway never signs for a user.
- src/mcp-tools/x-federation-join.ts: x_federation_join { code, relayHttp?,
relayWs?, keyFile? } — validates the code shape before any network call,
loads/creates the key, NIP-98 claim, NIP-42 verify, returns pubkey + role.
ADR-125: args take precedence over RUFLO_X_RELAY_HTTP / RUFLO_X_RELAY_WS /
RUFLO_NOSTR_KEY_FILE.
- CLI: `ruflo federation join --code v2.…`
- nostr-tools added as an optionalDependency (secp256k1/Schnorr is not in
node:crypto); the tool degrades with an install hint when absent. pnpm
lockfile regenerated in this PR (frozen-lockfile CI).
- 4 tests: 0600 key create/reuse, valid NIP-98 header (kind 27235, verifies,
bound to url+method+payload hash), malformed code rejected pre-network,
ADR-112 description. Suite: 16/16; tsc 0; env-var audit passes.
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_013u4pmL9ZUAXb6usVQgNo67
* fix(federation): default relay -> wss://relay.ruv.io; join test tolerates missing nostr-tools
- x-federation-join defaults now point at the canonical relay.ruv.io host (Cloud Run
domain mapping for buzz-relay); the raw run.app host stays routable for old clients.
- Validate the invite-code shape before the optional-dependency check so a bad code
fails fast whether or not nostr-tools is installed.
- The root Test Suite runs the CLI tests via root npm ci, which never installs the
CLI's optionalDependencies: the crypto cases are it.skipIf(!nostr-tools) so the
file no longer trips the CI test ratchet.
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_013u4pmL9ZUAXb6usVQgNo67
36 lines
2.4 KiB
TypeScript
36 lines
2.4 KiB
TypeScript
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
import { mkdtempSync, existsSync, statSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { loadOrCreateKey, nip98Header, xFederationJoinTools } from '../src/mcp-tools/x-federation-join.js';
|
|
// nostr-tools is an optionalDependency of the CLI and is NOT installed by the root `npm ci`
|
|
// (the CLI is a pnpm workspace, not a root npm workspace). The crypto-dependent cases are
|
|
// skipped when it is absent; the pure-logic cases always run.
|
|
const nt: any = await import('nostr-tools/pure').catch(() => null);
|
|
const { generateSecretKey, getPublicKey, verifyEvent } = nt ?? ({} as any);
|
|
describe('x_federation_join (self-service invite path)', () => {
|
|
afterEach(() => vi.unstubAllGlobals());
|
|
it.skipIf(!nt)('creates a 0600 key on first use and reuses it after', () => {
|
|
const f = join(mkdtempSync(join(tmpdir(), 'xj-')), 'nostr.key');
|
|
const a = loadOrCreateKey(nt, f); const b = loadOrCreateKey(nt, f);
|
|
expect(a.created).toBe(true); expect(b.created).toBe(false); expect(a.pubkey).toBe(b.pubkey);
|
|
expect(existsSync(f)).toBe(true); expect(statSync(f).mode & 0o777).toBe(0o600);
|
|
});
|
|
it.skipIf(!nt)('builds a valid NIP-98 header bound to url+method+payload hash', () => {
|
|
const sk = generateSecretKey(); const h = nip98Header(nt, sk, 'https://r/api/invites/claim', 'POST', '{"code":"v2.x"}');
|
|
const ev = JSON.parse(Buffer.from(h.replace(/^Nostr /, ''), 'base64').toString());
|
|
expect(ev.kind).toBe(27235); expect(verifyEvent(ev)).toBe(true); expect(ev.pubkey).toBe(getPublicKey(sk));
|
|
expect(ev.tags).toEqual(expect.arrayContaining([['u', 'https://r/api/invites/claim'], ['method', 'POST']]));
|
|
expect(ev.tags.find((t: string[]) => t[0] === 'payload')[1]).toMatch(/^[0-9a-f]{64}$/);
|
|
});
|
|
it('rejects malformed invite codes before any network call', async () => {
|
|
const f = vi.fn(); vi.stubGlobal('fetch', f);
|
|
await expect(xFederationJoinTools[0].handler({ code: 'not-a-code' }, {} as any)).rejects.toThrow(/v2\./);
|
|
expect(f).not.toHaveBeenCalled();
|
|
});
|
|
it('is registered with an ADR-112 description and requires code', () => {
|
|
const t = xFederationJoinTools[0]; expect(t.name).toBe('x_federation_join');
|
|
expect(t.description).toMatch(/Use when/); expect(t.description).toMatch(/wrong/); expect((t.inputSchema as any).required).toEqual(['code']);
|
|
});
|
|
});
|