From 26c12533fb5fa9426b52ad94a48a774cfdeb42ab Mon Sep 17 00:00:00 2001 From: hzhang Date: Sat, 16 May 2026 13:12:48 +0100 Subject: [PATCH] refactor(plugin): fabric-register is a script, not a tool Binding an agent's Fabric API key was an OpenClaw tool; make it a self-contained Node script installed to ~/.openclaw/bin/fabric-register instead. - bin/fabric-register.mjs: no plugin deps; AGENT_ID env wins, else --agent-id required; --api-key validated via POST /auth/agent/login; on success upserts ~/.openclaw/fabric-identity.json (format matches IdentityRegistry). Flags/env for center, identity-file, openclaw-path. - install.mjs: copy the script to ~/.openclaw/bin (chmod 0755) on install, remove on uninstall; Next-steps updated. - tools.ts: drop the fabric-register tool; ctxGuild error now points to the script / static accounts config. - README updated. Verified: missing-id -> exit 2; --agent-id and AGENT_ID both bind and write a valid identity file; bad key -> 401, no write. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 30 ++++++- bin/fabric-register.mjs | 164 +++++++++++++++++++++++++++++++++++++++ dist/fabric/src/tools.js | 34 ++------ install.mjs | 27 ++++++- src/tools.ts | 36 +++------ 5 files changed, 232 insertions(+), 59 deletions(-) create mode 100644 bin/fabric-register.mjs diff --git a/README.md b/README.md index 254215d..e03e8bb 100644 --- a/README.md +++ b/README.md @@ -38,8 +38,31 @@ host and OpenClaw's SSRF guard would block re-fetching it. Each agent has a Fabric Center **API key** (mint via Center CLI: `node dist/cli.js user apikey --email `). The key is exchanged -for a user session (`POST /auth/agent/login`). Bind a key to an agent with -the `fabric-register` tool, or via `channels.fabric.accounts`. +for a user session (`POST /auth/agent/login`). + +### Binding a key to an agent (one-time) + +Two ways, both write the same identity registry the transport reads: + +1. **Static config** — set `channels.fabric.accounts. = + { fabricApiKey, enabled }`. The agent never runs anything. +2. **`fabric-register` script** — installed to `~/.openclaw/bin/fabric-register` + by the installer (it is **not** an OpenClaw tool): + + ```bash + # agent id from $AGENT_ID (set in the agent runtime): + ~/.openclaw/bin/fabric-register --api-key fak_… + # or pass it explicitly: + ~/.openclaw/bin/fabric-register --agent-id --api-key fak_… + ``` + + It validates the key against Center, then writes + `~/.openclaw/fabric-identity.json`. One-time and persistent — *not* per + login; the plugin's transport logs in and stays connected on its own. + `AGENT_ID` env wins; otherwise `--agent-id` is required. Other flags: + `--center`, `--identity-file`, `--openclaw-path` (env equivalents: + `FABRIC_API_KEY`, `FABRIC_CENTER_API_BASE`, `FABRIC_IDENTITY_FILE`, + `OPENCLAW_PATH`). Restart the gateway afterwards. ## Config @@ -61,7 +84,8 @@ Then `openclaw gateway restart`. ## Tools -- `fabric-register` — bind this agent's Fabric API key +(Key binding is **not** a tool — see *Binding a key to an agent* above.) + - `create-chat-channel` (general) / `create-work-channel` (work) / `create-report-channel` (report) / `create-discussion-channel` (discuss) - `discussion-complete` — post a summary, then close the channel diff --git a/bin/fabric-register.mjs b/bin/fabric-register.mjs new file mode 100644 index 0000000..27f904d --- /dev/null +++ b/bin/fabric-register.mjs @@ -0,0 +1,164 @@ +#!/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 + * + * Flags / env: + * --agent-id (or env AGENT_ID; one of them required) + * --api-key (or env FABRIC_API_KEY; required) + * --center (or env FABRIC_CENTER_API_BASE; else openclaw.json; + * else http://localhost:7001/api) + * --identity-file (or env FABRIC_IDENTITY_FILE; else + * ~/.openclaw/fabric-identity.json) + * --openclaw-path (or env OPENCLAW_PATH; else ~/.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 $AGENT_ID is set + --api-key required (or env FABRIC_API_KEY) + --center Center API base (or env FABRIC_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 +`; + +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 '); + } + + const apiKey = + (typeof a['api-key'] === 'string' && a['api-key'].trim()) || + (process.env.FABRIC_API_KEY && process.env.FABRIC_API_KEY.trim()); + if (!apiKey) fail('missing --api-key (or env FABRIC_API_KEY)'); + + const openclawPath = resolve( + (typeof a['openclaw-path'] === 'string' && a['openclaw-path']) || + process.env.OPENCLAW_PATH || + join(homedir(), '.openclaw'), + ); + + // center api base: flag > env > openclaw.json > default + let center = + (typeof a.center === 'string' && a.center) || + process.env.FABRIC_CENTER_API_BASE || + ''; + 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']) || + process.env.FABRIC_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))); diff --git a/dist/fabric/src/tools.js b/dist/fabric/src/tools.js index 102dd0c..c93f35b 100644 --- a/dist/fabric/src/tools.js +++ b/dist/fabric/src/tools.js @@ -9,7 +9,9 @@ export function registerFabricTools(api, client, identity) { const ctxGuild = async (agentId, guildNodeId) => { const entry = identity.findByAgentId(agentId); if (!entry) - throw new Error(`agent ${agentId} not registered (call fabric-register)`); + throw new Error(`agent ${agentId} not registered — run: AGENT_ID=${agentId} ` + + `~/.openclaw/bin/fabric-register --api-key (or set ` + + `channels.fabric.accounts.${agentId}); then restart the gateway`); const session = await client.agentLogin(entry.fabricApiKey); const guild = session.guilds.find((g) => g.nodeId === guildNodeId); const token = session.guildAccessTokens.find((t) => t.guildNodeId === guildNodeId)?.token; @@ -17,32 +19,10 @@ export function registerFabricTools(api, client, identity) { throw new Error(`agent not a member of guild ${guildNodeId}`); return { session, guild, token }; }; - // fabric-register: bind this agent to a Fabric API key. - api.registerTool((ctx) => ({ - name: 'fabric-register', - description: "Register this agent's Fabric API key (minted via Center CLI `user apikey`).", - parameters: { - type: 'object', - additionalProperties: false, - required: ['fabricApiKey'], - properties: { - fabricApiKey: { type: 'string', description: 'Fabric Center API key (fak_…)' }, - }, - }, - execute: async (params) => { - const agentId = ctx.agentId; - if (!agentId) - return { ok: false, error: 'no agent context' }; - const session = await client.agentLogin(params.fabricApiKey); - identity.upsert({ - agentId, - fabricApiKey: params.fabricApiKey, - fabricUserId: session.user.id, - displayName: session.user.name, - }); - return { ok: true, user: session.user }; - }, - })); + // NOTE: binding an agent's Fabric API key is intentionally NOT a tool. + // It's a one-time step done out-of-band via the installed script + // ~/.openclaw/bin/fabric-register --api-key (AGENT_ID or --agent-id) + // or via static config (channels.fabric.accounts.). const makeCreate = (kind) => api.registerTool((ctx) => ({ name: `create-${kind}-channel`, description: `Create a Fabric ${kind} channel (x_type=${X_BY_KIND[kind]}).`, diff --git a/install.mjs b/install.mjs index ba038a8..064dfb7 100644 --- a/install.mjs +++ b/install.mjs @@ -10,7 +10,7 @@ */ import { execSync } from 'child_process'; -import { existsSync, mkdirSync, copyFileSync, readdirSync, rmSync } from 'fs'; +import { existsSync, mkdirSync, copyFileSync, readdirSync, rmSync, chmodSync } from 'fs'; import { dirname, join, resolve } from 'path'; import { fileURLToPath } from 'url'; import { homedir } from 'os'; @@ -109,12 +109,30 @@ function build() { ok('compiled -> dist/fabric'); } +function binTarget(base) { + return join(base, 'bin', 'fabric-register'); +} + +function installBinScript(base) { + const src = join(__dirname, 'bin', 'fabric-register.mjs'); + const dst = binTarget(base); + mkdirSync(dirname(dst), { recursive: true }); + copyFileSync(src, dst); + chmodSync(dst, 0o755); + ok(`fabric-register -> ${dst}`); +} + function clearInstall(base) { const dest = join(base, 'plugins', PLUGIN_ID); if (existsSync(dest)) { rmSync(dest, { recursive: true, force: true }); ok(`removed ${dest}`); } + const bin = binTarget(base); + if (existsSync(bin)) { + rmSync(bin, { force: true }); + ok(`removed ${bin}`); + } } function cleanupConfig(base) { const dest = join(base, 'plugins', PLUGIN_ID); @@ -149,6 +167,7 @@ function install() { ok(`plugin files -> ${dest}`); exec('npm install --omit=dev', { cwd: dest, silent: !opt.verbose }); ok('runtime deps installed'); + installBinScript(base); return { base, dest }; } @@ -200,8 +219,10 @@ function main() { console.log(''); log('Install complete. Next:', 'blue'); log(' 1. Mint an agent key: (in Center) node dist/cli.js user apikey --email ', 'cyan'); - log(' 2. openclaw gateway restart', 'cyan'); - log(' 3. As an agent, call the fabric-register tool with that key', 'cyan'); + log(' 2. Bind it to an agent (one-time), either:', 'cyan'); + log(' AGENT_ID= ~/.openclaw/bin/fabric-register --api-key ', 'cyan'); + log(' (or pass --agent-id ; or set channels.fabric.accounts.)', 'cyan'); + log(' 3. openclaw gateway restart', 'cyan'); console.log(''); } catch (e) { log(`\nInstall failed: ${e.message}`, 'red'); diff --git a/src/tools.ts b/src/tools.ts index ae9cf47..d1c4ead 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -25,7 +25,12 @@ export function registerFabricTools( // Resolve the calling agent's Fabric session + a guild's token/endpoint. const ctxGuild = async (agentId: string, guildNodeId: string) => { const entry = identity.findByAgentId(agentId); - if (!entry) throw new Error(`agent ${agentId} not registered (call fabric-register)`); + if (!entry) + throw new Error( + `agent ${agentId} not registered — run: AGENT_ID=${agentId} ` + + `~/.openclaw/bin/fabric-register --api-key (or set ` + + `channels.fabric.accounts.${agentId}); then restart the gateway`, + ); const session = await client.agentLogin(entry.fabricApiKey); const guild = session.guilds.find((g) => g.nodeId === guildNodeId); const token = session.guildAccessTokens.find((t) => t.guildNodeId === guildNodeId)?.token; @@ -33,31 +38,10 @@ export function registerFabricTools( return { session, guild, token }; }; - // fabric-register: bind this agent to a Fabric API key. - api.registerTool((ctx: Ctx) => ({ - name: 'fabric-register', - description: "Register this agent's Fabric API key (minted via Center CLI `user apikey`).", - parameters: { - type: 'object', - additionalProperties: false, - required: ['fabricApiKey'], - properties: { - fabricApiKey: { type: 'string', description: 'Fabric Center API key (fak_…)' }, - }, - }, - execute: async (params: { fabricApiKey: string }) => { - const agentId = ctx.agentId; - if (!agentId) return { ok: false, error: 'no agent context' }; - const session = await client.agentLogin(params.fabricApiKey); - identity.upsert({ - agentId, - fabricApiKey: params.fabricApiKey, - fabricUserId: session.user.id, - displayName: session.user.name, - }); - return { ok: true, user: session.user }; - }, - })); + // NOTE: binding an agent's Fabric API key is intentionally NOT a tool. + // It's a one-time step done out-of-band via the installed script + // ~/.openclaw/bin/fabric-register --api-key (AGENT_ID or --agent-id) + // or via static config (channels.fabric.accounts.). const makeCreate = (kind: 'chat' | 'work' | 'report' | 'discussion') => api.registerTool((ctx: Ctx) => ({