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) <noreply@anthropic.com>
This commit is contained in:
30
README.md
30
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 <agent-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.<agentId> =
|
||||
{ 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 <agent> --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
|
||||
|
||||
164
bin/fabric-register.mjs
Normal file
164
bin/fabric-register.mjs
Normal file
@@ -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 <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)));
|
||||
34
dist/fabric/src/tools.js
vendored
34
dist/fabric/src/tools.js
vendored
@@ -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 <fak_…> (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 <fak_…> (AGENT_ID or --agent-id)
|
||||
// or via static config (channels.fabric.accounts.<agentId>).
|
||||
const makeCreate = (kind) => api.registerTool((ctx) => ({
|
||||
name: `create-${kind}-channel`,
|
||||
description: `Create a Fabric ${kind} channel (x_type=${X_BY_KIND[kind]}).`,
|
||||
|
||||
27
install.mjs
27
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 <agent-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=<agent> ~/.openclaw/bin/fabric-register --api-key <fak_…>', 'cyan');
|
||||
log(' (or pass --agent-id <agent>; or set channels.fabric.accounts.<agent>)', 'cyan');
|
||||
log(' 3. openclaw gateway restart', 'cyan');
|
||||
console.log('');
|
||||
} catch (e) {
|
||||
log(`\nInstall failed: ${e.message}`, 'red');
|
||||
|
||||
36
src/tools.ts
36
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 <fak_…> (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 <fak_…> (AGENT_ID or --agent-id)
|
||||
// or via static config (channels.fabric.accounts.<agentId>).
|
||||
|
||||
const makeCreate = (kind: 'chat' | 'work' | 'report' | 'discussion') =>
|
||||
api.registerTool((ctx: Ctx) => ({
|
||||
|
||||
Reference in New Issue
Block a user