feat(fabric): dynamic-subscription via fabric-register openclaw tool

The plugin manifest declared `fabric-register` as a tool name but
tools.ts never registered it — recruitment fell through to the
standalone `/root/.openclaw/bin/fabric-register` binary, which writes
~/.openclaw/fabric-identity.json correctly but exits without notifying
the running plugin. That left fabric inbound's subscription as a
connect-time snapshot: every new agent required a gateway restart
between `new-agent` and the interview's sub-discussion or the message
had no socket to dispatch on.

Wire the full path:
  - `FabricInbound.addAccount(entry)` — login, upsert identity, open socket(s),
    track per-agent so removeAccount can teardown cleanly. Idempotent: a
    second call replaces the previous socket (used post-onboard when the
    agent rotates off the shared `interviewee` placeholder onto its own
    apikey).
  - `FabricInbound.removeAccount(agentId)` — disconnect sockets, clear
    timers + per-agent caches.
  - `__fabric.addAccount` / `removeAccount` — cross-plugin bridge so the
    `fabric-register` tool can reach the live FabricInbound instance from
    its tool handler context.
  - `fabric-register` openclaw tool — validates apiKey, calls
    `__fabric.addAccount`, returns `{ok, fabricUserId, displayName}`.
    Accepts `agentId` arg so recruitment can bind on behalf of a
    freshly-created agent before that agent has a session of its own.

Removes the "restart the gateway" advice from the ctxGuild
"agent not registered" error message — operators should now call the
tool path instead.

After this lands + the ClawSkills register-agent script flip (separate
commit), `recruitment.new-agent` -> interviewer sub-discussion runs
without a gateway restart in between.
This commit is contained in:
h z
2026-06-01 08:56:53 +01:00
parent 260d50196b
commit 893b93198d
3 changed files with 168 additions and 9 deletions

View File

@@ -96,13 +96,29 @@ export default defineChannelPluginEntry({
// fall back to "assume DM" — fail closed on unknown.
{
const _G = globalThis as Record<string, unknown>;
_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: { agentId: string; fabricApiKey: string }) => {
if (!inbound) throw new Error('fabric inbound not ready yet (gateway not started?)');
await inbound.addAccount(entry);
},
removeAccount: (agentId: string) => {
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', () => {
try { flushChannelMeta(); } catch { /* ignore */ }
});
api.logger.info('fabric: __fabric cross-plugin API installed (getChannelType)');
api.logger.info('fabric: __fabric cross-plugin API installed (getChannelType + addAccount + removeAccount)');
}
api.on('gateway_start', () => {