feat(gateway): register fabric.register gateway method for live no-restart registration

Recruitment's register-agent step is a plain shell step (no LLM turn), so it
cannot invoke the `fabric-register` TOOL (tool only fires inside an agent
turn) and there is no `openclaw tools call` CLI. It previously fell back to
the standalone bootstrap binary, which writes fabric-identity.json but cannot
notify the running plugin -> the new agent's inbound socket only came up after
a gateway restart.

This adds an in-process gateway method `fabric.register` (scope:
operator.write) whose handler runs inbound.addAccount: validates the key,
persists identity, and brings the inbound socket up immediately. The script
now calls `openclaw gateway call fabric.register --params ...` and only falls
back to the bootstrap binary if the method is unavailable.

Also resyncs committed dist/ to source (sub-discussion-hook/store + tools/
inbound were source-committed but their dist artifacts were stale).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
h z
2026-06-07 21:04:38 +01:00
parent 40c9cb5740
commit f8c8c21727
7 changed files with 737 additions and 21 deletions

60
dist/fabric/index.js vendored
View File

@@ -14,6 +14,8 @@ import { FabricClient } from './src/fabric-client.js';
import { IdentityRegistry } from './src/identity.js';
import { syncFabricCommands } from './src/command-sync.js';
import { PresenceSync } from './src/presence-sync.js';
import { SubDiscussionStore } from './src/sub-discussion-store.js';
import { registerSubDiscussionHook } from './src/sub-discussion-hook.js';
import path from 'node:path';
import os from 'node:os';
let runtimeRef = null;
@@ -40,10 +42,17 @@ export default defineChannelPluginEntry({
const centerApiBase = cfg.channels?.fabric?.centerApiBase ?? 'http://localhost:7001/api';
const idFile = api.pluginConfig?.identityFilePath ??
path.join(os.homedir(), '.openclaw', 'fabric-identity.json');
const subDiscussionFile = path.join(os.homedir(), '.openclaw', 'fabric-sub-discussion.json');
// tools operate against a default Center; per-account keys come from config
const client = new FabricClient(centerApiBase);
const identity = new IdentityRegistry(idFile);
registerFabricTools({ registerTool: (d) => api.registerTool(d), logger: api.logger }, client, identity);
const subDiscussion = new SubDiscussionStore(subDiscussionFile);
registerFabricTools({ registerTool: (d) => api.registerTool(d), logger: api.logger }, client, identity, subDiscussion, cfg);
// Per-(agent, channel) prompt injection for sub-discussion channels.
// Runs as a sibling to PrismFacet's before_prompt_build hook (and
// ClawPrompts' fabric-chat-injector); openclaw composes
// appendSystemContext from all registered handlers.
registerSubDiscussionHook({ on: api.on, logger: api.logger }, subDiscussion, identity);
// Cross-plugin API: globalThis.__fabric
// Consumed by ClawPrompts' fabric-chat-injector to narrow its prompt
// injection to DM-typed channels only. The channel-meta cache is
@@ -56,7 +65,25 @@ export default defineChannelPluginEntry({
// fall back to "assume DM" — fail closed on unknown.
{
const _G = globalThis;
_G['__fabric'] = { getChannelType };
_G['__fabric'] = {
getChannelType,
// Dynamic-subscription bridges: tools (notably `fabric-register`)
// call these to add/remove an account's inbound socket without
// a gateway restart. Both delegate to the live FabricInbound
// instance via the module-level `inbound` closure variable; the
// closures stay valid across gateway_start / gateway_stop
// because we re-assign the variable, not the property.
addAccount: async (entry) => {
if (!inbound)
throw new Error('fabric inbound not ready yet (gateway not started?)');
await inbound.addAccount(entry);
},
removeAccount: (agentId) => {
if (!inbound)
return;
inbound.removeAccount(agentId);
},
};
// Flush channel-meta cache when the gateway shuts down so
// recently-recorded xType entries don't get lost.
api.on('gateway_stop', () => {
@@ -65,8 +92,35 @@ export default defineChannelPluginEntry({
}
catch { /* ignore */ }
});
api.logger.info('fabric: __fabric cross-plugin API installed (getChannelType)');
api.logger.info('fabric: __fabric cross-plugin API installed (getChannelType + addAccount + removeAccount)');
}
// CLI-invocable live registration, callable from a shell script via
// openclaw gateway call fabric.register --params '{"agentId":"…","apiKey":"fak_…"}'
// The `fabric-register` TOOL only fires inside an agent turn, and there is
// no `openclaw tools call` CLI — so recruitment's `register-agent` script
// (a plain shell step, no LLM turn) had to fall back to the standalone
// binary, which can't notify the running plugin → needed a gateway
// restart. This gateway method runs in-process: inbound.addAccount
// validates the key, persists identity, and brings the socket up live —
// no restart.
api.registerGatewayMethod('fabric.register', async ({ params, respond }) => {
const p = (params ?? {});
if (!p.agentId || !p.apiKey) {
respond(false, { ok: false }, { code: 'INVALID_REQUEST', message: 'agentId and apiKey required' });
return;
}
if (!inbound) {
respond(false, { ok: false }, { code: 'UNAVAILABLE', message: 'fabric inbound not ready (gateway still starting?)' });
return;
}
try {
await inbound.addAccount({ agentId: p.agentId, fabricApiKey: p.apiKey });
respond(true, { ok: true, agentId: p.agentId });
}
catch (err) {
respond(false, { ok: false }, { code: 'UNAVAILABLE', message: `fabric-register failed: ${String(err)}` });
}
}, { scope: 'operator.write' });
api.on('gateway_start', () => {
const _G = globalThis;
if (_G._fabricInboundStarted)