#!/usr/bin/env node /** * fabric-register — bind an OpenClaw agent to a Fabric Center API key. * * One-time, self-contained (no plugin deps). Installed to * ~/.openclaw/bin/fabric-register by the plugin installer. * * AGENT_ID is read from the environment if set; otherwise --agent-id is * required. The API key is validated against Center (POST * /auth/agent/login) and, on success, written to the plugin's identity * file so the Fabric channel plugin can connect that agent. * * Usage: * fabric-register --api-key fak_xxx # uses $AGENT_ID * fabric-register --agent-id echo --api-key fak_xxx * * Only AGENT_ID is read from the environment; everything else is a flag. * * Flags: * --agent-id required unless the AGENT_ID env var is set * --api-key required (flag only — never from the environment) * --center else openclaw.json channels.fabric.centerApiBase; * else http://localhost:7001/api * --identity-file default ~/.openclaw/fabric-identity.json * --openclaw-path default ~/.openclaw * -h | --help */ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs'; import { dirname, join, resolve } from 'node:path'; import { homedir } from 'node:os'; function parseArgs(argv) { const out = {}; for (let i = 0; i < argv.length; i++) { const a = argv[i]; if (a === '-h' || a === '--help') out.help = true; else if (a.startsWith('--')) { const key = a.slice(2); const v = argv[i + 1]; if (v === undefined || v.startsWith('--')) out[key] = true; else { out[key] = v; i++; } } } return out; } const HELP = `fabric-register — bind an OpenClaw agent to a Fabric Center API key fabric-register --api-key fak_xxx # agent id from $AGENT_ID fabric-register --agent-id --api-key fak_xxx --agent-id required unless the AGENT_ID env var is set --api-key required (flag only — never read from the env) --center Center API base (else openclaw.json channels.fabric.centerApiBase; else http://localhost:7001/api) --identity-file default ~/.openclaw/fabric-identity.json --openclaw-path default ~/.openclaw -h, --help Only AGENT_ID is taken from the environment; everything else is a flag. `; function fail(msg) { console.error(`fabric-register: ${msg}`); process.exit(2); } async function main() { const a = parseArgs(process.argv.slice(2)); if (a.help) { process.stdout.write(HELP); return; } // agent id: env AGENT_ID wins; else --agent-id is required. const agentId = (process.env.AGENT_ID && process.env.AGENT_ID.trim()) || (typeof a['agent-id'] === 'string' && a['agent-id'].trim()); if (!agentId) { fail('no agent id: set the AGENT_ID environment variable or pass --agent-id '); } // api key: flag ONLY — never from the environment. const apiKey = typeof a['api-key'] === 'string' && a['api-key'].trim(); if (!apiKey) fail('missing --api-key (flag only)'); const openclawPath = resolve( (typeof a['openclaw-path'] === 'string' && a['openclaw-path']) || join(homedir(), '.openclaw'), ); // center api base: flag > openclaw.json > default let center = (typeof a.center === 'string' && a.center) || ''; if (!center) { try { const cfg = JSON.parse(readFileSync(join(openclawPath, 'openclaw.json'), 'utf8')); center = cfg?.channels?.fabric?.centerApiBase || ''; } catch { /* fall through to default */ } } if (!center) center = 'http://localhost:7001/api'; center = center.replace(/\/+$/, ''); const identityFile = resolve( (typeof a['identity-file'] === 'string' && a['identity-file']) || join(openclawPath, 'fabric-identity.json'), ); // 1) validate the key against Center (also resolves the Fabric identity) let session; try { const res = await fetch(`${center}/auth/agent/login`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ apiKey }), }); if (!res.ok) { const t = await res.text().catch(() => ''); fail(`Center rejected the key: POST ${center}/auth/agent/login -> ${res.status} ${t}`); } session = await res.json(); } catch (e) { fail(`could not reach Center at ${center}: ${e?.message || e}`); } // 2) upsert into the identity file (merge by agentId) let file = { entries: [] }; if (existsSync(identityFile)) { try { const parsed = JSON.parse(readFileSync(identityFile, 'utf8')); if (parsed && Array.isArray(parsed.entries)) file = parsed; } catch { /* corrupt -> overwrite */ } } const entry = { agentId, fabricApiKey: apiKey, fabricUserId: session?.user?.id, displayName: session?.user?.name, }; const i = file.entries.findIndex((e) => e && e.agentId === agentId); if (i >= 0) file.entries[i] = { ...file.entries[i], ...entry }; else file.entries.push(entry); mkdirSync(dirname(identityFile), { recursive: true }); writeFileSync(identityFile, JSON.stringify(file, null, 2)); console.log( `fabric-register: bound agent "${agentId}" -> Fabric user ` + `${session?.user?.email || session?.user?.id} (${identityFile}). ` + `Restart the gateway to connect: openclaw gateway restart`, ); } main().catch((e) => fail(e?.message || String(e)));