Files
Fabric.OpenclawPlugin/bin/fabric-register.mjs
hzhang aaabb0ddb0 feat(plugin): fabric-canvas tool; fabric-register env=AGENT_ID only
- bin/fabric-register.mjs: only AGENT_ID is read from the environment;
  --api-key is flag-only (no FABRIC_API_KEY); dropped FABRIC_CENTER_API_BASE
  / FABRIC_IDENTITY_FILE / OPENCLAW_PATH env fallbacks (flags + sensible
  defaults; --center still falls back to openclaw.json).
- New fabric-canvas tool (one tool, four actions): read / share / update /
  close the channel's single pinned canvas. Backed by FabricClient
  get/share/update/removeCanvas (GET/PUT/PATCH/DELETE; empty 2xx body ->
  null). update/close are sharer-only server-side.
- README updated.

Verified: client-level smoke against the running guild —
read(empty→null) → share(v1) → read → update(v2) → close(→null) all pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 15:28:13 +01:00

162 lines
5.4 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
*
* Only AGENT_ID is read from the environment; everything else is a flag.
*
* Flags:
* --agent-id <id> required unless the AGENT_ID env var is set
* --api-key <fak_…> required (flag only — never from the environment)
* --center <url> 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
*/
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 the AGENT_ID env var is set
--api-key <fak_…> required (flag only — never read from the env)
--center <url> 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
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 <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 <fak_…> (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)));