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) <noreply@anthropic.com>
165 lines
5.5 KiB
JavaScript
165 lines
5.5 KiB
JavaScript
#!/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 <id> (or env AGENT_ID; one of them required)
|
|
* --api-key <fak_…> (or env FABRIC_API_KEY; required)
|
|
* --center <url> (or env FABRIC_CENTER_API_BASE; else openclaw.json;
|
|
* else http://localhost:7001/api)
|
|
* --identity-file <path> (or env FABRIC_IDENTITY_FILE; else
|
|
* ~/.openclaw/fabric-identity.json)
|
|
* --openclaw-path <dir> (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 <id> --api-key fak_xxx
|
|
|
|
--agent-id <id> required unless $AGENT_ID is set
|
|
--api-key <fak_…> required (or env FABRIC_API_KEY)
|
|
--center <url> Center API base (or env FABRIC_CENTER_API_BASE;
|
|
else openclaw.json channels.fabric.centerApiBase;
|
|
else http://localhost:7001/api)
|
|
--identity-file <path> default ~/.openclaw/fabric-identity.json
|
|
--openclaw-path <dir> 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 <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 <fak_…> (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)));
|