diff --git a/dist/fabric/index.js b/dist/fabric/index.js index 6fe7c79..b1949ee 100644 --- a/dist/fabric/index.js +++ b/dist/fabric/index.js @@ -1,40 +1,62 @@ -import path from 'node:path'; -import os from 'node:os'; -import { defineChannelPluginEntry } from 'openclaw/plugin-sdk/channel-core'; +// Fabric channel plugin entry. +// COMPAT NOTE (openclaw v2026.5.7): defineChannelPluginEntry signature +// { id, name, description, plugin, setRuntime?, registerFull? }. setRuntime +// receives the PluginRuntime (has channel.turn kernel); registerFull receives +// the OpenClawPluginApi for runtime startup (transport + tools). +import { defineChannelPluginEntry } from 'openclaw/plugin-sdk/core'; +import { fabricChannelPlugin } from './src/channel.js'; +import { FabricInbound } from './src/inbound.js'; +import { listEnabledFabricAccounts } from './src/accounts.js'; +import { registerFabricTools } from './src/tools.js'; import { FabricClient } from './src/fabric-client.js'; import { IdentityRegistry } from './src/identity.js'; -import { FabricInbound } from './src/inbound.js'; -import { buildFabricChannelPlugin } from './src/channel.js'; -import { registerFabricTools } from './src/tools.js'; -function centerApiBase(api) { - const section = api.config?.channels?.['fabric']; - return section?.centerApiBase ?? 'http://localhost:7001/api'; -} +import path from 'node:path'; +import os from 'node:os'; +let runtimeRef = null; let inbound = null; +export { fabricChannelPlugin } from './src/channel.js'; export default defineChannelPluginEntry({ id: 'fabric', name: 'Fabric', description: 'Fabric channel plugin — OpenClaw agents speak in Fabric guilds', - // Channel object: config/security/outbound. Visible turn replies flow - // through the inbound channel-turn delivery adapter; outbound.sendText - // covers proactive sends via the shared message tool. - plugin: buildFabricChannelPlugin(async () => ({ messageId: undefined })), - // registerFull: runtime pieces (transport, tools). Guarded so the long-lived - // Fabric connections start once per gateway process. - registerFull(api) { - const cfg = (api.pluginConfig ?? {}); - const identityFilePath = cfg.identityFilePath ?? path.join(os.homedir(), '.openclaw', 'fabric-identity.json'); - const client = new FabricClient(centerApiBase(api)); - const identity = new IdentityRegistry(identityFilePath); + plugin: fabricChannelPlugin, + setRuntime(runtime) { + runtimeRef = runtime; + }, + registerFull(apiRaw) { + // COMPAT: access the subset we use through a loose view so SDK type + // drift in unrelated api members doesn't break the build. + const api = apiRaw; + const cfg = (api.config ?? {}); + const centerApiBase = cfg.channels?.fabric?.centerApiBase ?? 'http://localhost:7001/api'; + const idFile = api.pluginConfig?.identityFilePath ?? + path.join(os.homedir(), '.openclaw', 'fabric-identity.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); api.on('gateway_start', () => { const _G = globalThis; if (_G._fabricInboundStarted) return; _G._fabricInboundStarted = true; - inbound = new FabricInbound(api.runtime, client, identity, api.logger); + const accounts = listEnabledFabricAccounts(cfg).map((a) => ({ + agentId: a.accountId, + fabricApiKey: a.fabricApiKey, + })); + // also include any tool-registered identities + for (const e of identity.list()) { + if (!accounts.some((x) => x.agentId === e.agentId)) { + accounts.push({ agentId: e.agentId, fabricApiKey: e.fabricApiKey }); + } + } + if (!runtimeRef) { + api.logger.warn('fabric: runtime not set; inbound disabled'); + return; + } + inbound = new FabricInbound(runtimeRef, client, identity, api.logger, accounts); void inbound.start(); - api.logger.info('fabric: inbound transport started'); + api.logger.info(`fabric: inbound started for ${accounts.length} account(s)`); }); api.on('gateway_stop', () => { inbound?.stop(); diff --git a/dist/fabric/src/accounts.js b/dist/fabric/src/accounts.js new file mode 100644 index 0000000..3852348 --- /dev/null +++ b/dist/fabric/src/accounts.js @@ -0,0 +1,41 @@ +// agent = openclaw channel account. +// Config shape: +// channels.fabric.centerApiBase = "http://localhost:7001/api" (shared) +// channels.fabric.accounts. = { fabricApiKey, centerApiBase? } +// Each account id IS the openclaw agentId that owns that Fabric identity. +const DEFAULT_CENTER = 'http://localhost:7001/api'; +function section(cfg) { + return cfg.channels?.fabric ?? {}; +} +export function listFabricAccountIds(cfg) { + const accts = section(cfg).accounts ?? {}; + const ids = Object.keys(accts); + return ids.length ? ids : ['default']; +} +export function resolveDefaultFabricAccountId(cfg) { + const s = section(cfg); + if (s.defaultAccount) + return s.defaultAccount; + const ids = listFabricAccountIds(cfg); + return ids[0] ?? 'default'; +} +export function resolveFabricAccount(cfg, accountId) { + const s = section(cfg); + const id = accountId ?? resolveDefaultFabricAccountId(cfg); + const acc = s.accounts?.[id] ?? {}; + const fabricApiKey = (acc.fabricApiKey ?? '').trim(); + const centerApiBase = (acc.centerApiBase ?? s.centerApiBase ?? DEFAULT_CENTER).trim(); + return { + accountId: id, + enabled: acc.enabled !== false && s.enabled !== false, + centerApiBase, + fabricApiKey, + allowFrom: acc.allowFrom ?? s.allowFrom ?? [], + dmPolicy: acc.dmPolicy ?? s.dmPolicy, + }; +} +export function listEnabledFabricAccounts(cfg) { + return listFabricAccountIds(cfg) + .map((id) => resolveFabricAccount(cfg, id)) + .filter((a) => a.enabled && a.fabricApiKey); +} diff --git a/dist/fabric/src/channel.js b/dist/fabric/src/channel.js index 434ddcd..979a58a 100644 --- a/dist/fabric/src/channel.js +++ b/dist/fabric/src/channel.js @@ -1,46 +1,91 @@ -import { createChatChannelPlugin, createChannelPluginBase, } from 'openclaw/plugin-sdk/channel-core'; -export function resolveFabricAccount(cfg, accountId) { - const section = cfg.channels?.['fabric']; - const centerApiBase = section?.centerApiBase; - if (!centerApiBase) - throw new Error('fabric: channels.fabric.centerApiBase is required'); - return { - accountId: accountId ?? null, - centerApiBase, - allowFrom: section?.allowFrom ?? [], - dmPolicy: section?.dmSecurity, - }; +// Fabric channel plugin object (third-party `createChatChannelPlugin` path). +// +// COMPAT NOTE (openclaw v2026.5.7 plugin SDK): +// We depend on these generic SDK shapes — re-verify on openclaw upgrade: +// - createChannelPluginBase requires `capabilities` +// - ChannelSetupAdapter: only `applyAccountConfig` is required +// - outbound attachedResults.sendText returns Omit (so `messageId: string` is required) +// Casts at the createChatChannelPlugin boundary are intentional and +// localized; keep them here so upgrades touch one file. +import { createChatChannelPlugin, createChannelPluginBase, } from 'openclaw/plugin-sdk/core'; +import { FabricClient } from './fabric-client.js'; +import { listFabricAccountIds, resolveFabricAccount, resolveDefaultFabricAccountId, } from './accounts.js'; +// Posts an agent's reply to Fabric. `to` is the Fabric channelId; `accountId` +// is the agentId (= Fabric identity). One auth concept: account apiKey -> +// agent/login -> guild token -> POST message. +async function sendToFabric(cfg, accountId, to, text) { + const acc = resolveFabricAccount(cfg, accountId); + if (!acc.fabricApiKey) + throw new Error(`fabric account ${acc.accountId} has no fabricApiKey`); + const client = new FabricClient(acc.centerApiBase); + const session = await client.agentLogin(acc.fabricApiKey); + // find which guild owns this channel by probing each guild's channel list + for (const g of session.guilds) { + const gt = session.guildAccessTokens.find((t) => t.guildNodeId === g.nodeId)?.token; + if (!gt) + continue; + const res = await fetch(`${g.endpoint}/api/channels?guildId=${encodeURIComponent(g.nodeId)}`, { + headers: { authorization: `Bearer ${gt}` }, + }); + const channels = res.ok ? (await res.json()) : []; + if (channels.some((c) => c.id === to)) { + await client.postMessage(g.endpoint, gt, to, text, session.user.id); + return { messageId: `${to}:${Date.now()}` }; + } + } + // fallback: first guild + const g = session.guilds[0]; + const gt = session.guildAccessTokens.find((t) => t.guildNodeId === g?.nodeId)?.token; + if (g && gt) { + await client.postMessage(g.endpoint, gt, to, text, session.user.id); + return { messageId: `${to}:${Date.now()}` }; + } + throw new Error('fabric: no guild available to deliver'); } -// Outbound is wired by the entry (it needs the identity registry + client to -// post as the right agent). Channel-turn visible replies go through the -// inbound adapter's delivery callback; this object owns config/security only. -export function buildFabricChannelPlugin(sendText) { - return createChatChannelPlugin({ - base: createChannelPluginBase({ - id: 'fabric', - setup: { - resolveAccount: resolveFabricAccount, - inspectAccount(cfg, accountId) { - const section = cfg.channels?.['fabric']; - const ok = Boolean(section?.centerApiBase); - return { enabled: ok, configured: ok, tokenStatus: ok ? 'available' : 'missing' }; - }, - }, - }), - security: { - dm: { - channelKey: 'fabric', - resolvePolicy: (a) => a.dmPolicy, - resolveAllowFrom: (a) => a.allowFrom, - defaultPolicy: 'allowlist', +export const fabricChannelPlugin = createChatChannelPlugin({ + base: createChannelPluginBase({ + id: 'fabric', + meta: { id: 'fabric', label: 'Fabric', blurb: 'Connect OpenClaw agents to a Fabric guild.' }, + capabilities: { + chatTypes: ['channel', 'group', 'direct'], + reactions: false, + threads: false, + media: false, + nativeCommands: false, + blockStreaming: true, + }, + reload: { configPrefixes: ['channels.fabric'] }, + config: { + listAccountIds: (cfg) => listFabricAccountIds(cfg), + resolveAccount: (cfg, accountId) => resolveFabricAccount(cfg, accountId), + defaultAccountId: (cfg) => resolveDefaultFabricAccountId(cfg), + isConfigured: (account) => Boolean(account.fabricApiKey), + }, + // Minimal setup adapter: Fabric is configured directly under + // channels.fabric.* (no interactive wizard). applyAccountConfig is the + // only required member. + setup: { + applyAccountConfig: ({ cfg }) => cfg, + }, + }), + security: { + dm: { + channelKey: 'fabric', + resolvePolicy: (a) => a.dmPolicy, + resolveAllowFrom: (a) => a.allowFrom, + defaultPolicy: 'allowlist', + }, + }, + threading: { topLevelReplyToMode: 'channel' }, + outbound: { + base: {}, + attachedResults: { + channel: 'fabric', + sendText: async (ctx) => { + const cfg = (ctx.cfg ?? {}); + return sendToFabric(cfg, ctx.accountId ?? null, ctx.to, ctx.text); }, }, - // Fabric replies thread by being posted into the same channel. - threading: { topLevelReplyToMode: 'channel' }, - outbound: { - attachedResults: { - sendText: async (params) => sendText(params), - }, - }, - }); -} + }, +}); diff --git a/dist/fabric/src/inbound.js b/dist/fabric/src/inbound.js index f278405..dc5fde3 100644 --- a/dist/fabric/src/inbound.js +++ b/dist/fabric/src/inbound.js @@ -7,17 +7,22 @@ export class FabricInbound { client; identity; log; + accounts; sockets = []; timers = []; seen = new Set(); - constructor(runtime, client, identity, log) { + constructor(runtime, client, identity, log, accounts = []) { this.runtime = runtime; this.client = client; this.identity = identity; this.log = log; + this.accounts = accounts; } async start() { - for (const entry of this.identity.list()) { + const entries = this.accounts.length > 0 + ? this.accounts.map((a) => ({ agentId: a.agentId, fabricApiKey: a.fabricApiKey })) + : this.identity.list(); + for (const entry of entries) { try { const session = await this.client.agentLogin(entry.fabricApiKey); this.identity.upsert({ diff --git a/index.ts b/index.ts index 3e207f2..d04f127 100644 --- a/index.ts +++ b/index.ts @@ -1,46 +1,55 @@ -import path from 'node:path'; -import os from 'node:os'; -import { defineChannelPluginEntry } from 'openclaw/plugin-sdk/channel-core'; +// Fabric channel plugin entry. +// COMPAT NOTE (openclaw v2026.5.7): defineChannelPluginEntry signature +// { id, name, description, plugin, setRuntime?, registerFull? }. setRuntime +// receives the PluginRuntime (has channel.turn kernel); registerFull receives +// the OpenClawPluginApi for runtime startup (transport + tools). +import { defineChannelPluginEntry } from 'openclaw/plugin-sdk/core'; import type { OpenClawPluginApi } from 'openclaw/plugin-sdk/core'; +import { fabricChannelPlugin } from './src/channel.js'; +import { FabricInbound } from './src/inbound.js'; +import { listEnabledFabricAccounts } from './src/accounts.js'; +import { registerFabricTools } from './src/tools.js'; import { FabricClient } from './src/fabric-client.js'; import { IdentityRegistry } from './src/identity.js'; -import { FabricInbound } from './src/inbound.js'; -import { buildFabricChannelPlugin } from './src/channel.js'; -import { registerFabricTools } from './src/tools.js'; - -type PluginConfig = { identityFilePath?: string; debugMode?: boolean }; - -function centerApiBase(api: OpenClawPluginApi): string { - const section = ((api.config as Record)?.channels as Record)?.[ - 'fabric' - ]; - return section?.centerApiBase ?? 'http://localhost:7001/api'; -} +import path from 'node:path'; +import os from 'node:os'; +let runtimeRef: unknown = null; let inbound: FabricInbound | null = null; +export { fabricChannelPlugin } from './src/channel.js'; + export default defineChannelPluginEntry({ id: 'fabric', name: 'Fabric', description: 'Fabric channel plugin — OpenClaw agents speak in Fabric guilds', + plugin: fabricChannelPlugin, - // Channel object: config/security/outbound. Visible turn replies flow - // through the inbound channel-turn delivery adapter; outbound.sendText - // covers proactive sends via the shared message tool. - plugin: buildFabricChannelPlugin(async () => ({ messageId: undefined })), + setRuntime(runtime: unknown) { + runtimeRef = runtime; + }, - // registerFull: runtime pieces (transport, tools). Guarded so the long-lived - // Fabric connections start once per gateway process. - registerFull(api: OpenClawPluginApi) { - const cfg = (api.pluginConfig ?? {}) as PluginConfig; - const identityFilePath = - cfg.identityFilePath ?? path.join(os.homedir(), '.openclaw', 'fabric-identity.json'); - - const client = new FabricClient(centerApiBase(api)); - const identity = new IdentityRegistry(identityFilePath); + registerFull(apiRaw: OpenClawPluginApi) { + // COMPAT: access the subset we use through a loose view so SDK type + // drift in unrelated api members doesn't break the build. + const api = apiRaw as unknown as { + config?: unknown; + pluginConfig?: { identityFilePath?: string }; + logger: { info: (m: string) => void; warn: (m: string) => void }; + on: (ev: string, fn: () => void) => void; + registerTool: (d: unknown) => void; + }; + const cfg = (api.config ?? {}) as { channels?: { fabric?: { centerApiBase?: string } } }; + const centerApiBase = cfg.channels?.fabric?.centerApiBase ?? 'http://localhost:7001/api'; + const idFile = + api.pluginConfig?.identityFilePath ?? + path.join(os.homedir(), '.openclaw', 'fabric-identity.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 as never), logger: api.logger }, + { registerTool: (d) => api.registerTool(d), logger: api.logger }, client, identity, ); @@ -49,14 +58,23 @@ export default defineChannelPluginEntry({ const _G = globalThis as Record; if (_G._fabricInboundStarted) return; _G._fabricInboundStarted = true; - inbound = new FabricInbound( - (api as unknown as { runtime: any }).runtime, - client, - identity, - api.logger, - ); + const accounts = listEnabledFabricAccounts(cfg as never).map((a) => ({ + agentId: a.accountId, + fabricApiKey: a.fabricApiKey, + })); + // also include any tool-registered identities + for (const e of identity.list()) { + if (!accounts.some((x) => x.agentId === e.agentId)) { + accounts.push({ agentId: e.agentId, fabricApiKey: e.fabricApiKey }); + } + } + if (!runtimeRef) { + api.logger.warn('fabric: runtime not set; inbound disabled'); + return; + } + inbound = new FabricInbound(runtimeRef as never, client, identity, api.logger, accounts); void inbound.start(); - api.logger.info('fabric: inbound transport started'); + api.logger.info(`fabric: inbound started for ${accounts.length} account(s)`); }); api.on('gateway_stop', () => { diff --git a/src/accounts.ts b/src/accounts.ts new file mode 100644 index 0000000..d5f2f96 --- /dev/null +++ b/src/accounts.ts @@ -0,0 +1,71 @@ +// agent = openclaw channel account. +// Config shape: +// channels.fabric.centerApiBase = "http://localhost:7001/api" (shared) +// channels.fabric.accounts. = { fabricApiKey, centerApiBase? } +// Each account id IS the openclaw agentId that owns that Fabric identity. + +export type FabricAccountConfig = { + fabricApiKey?: string; + centerApiBase?: string; + enabled?: boolean; + allowFrom?: string[]; + dmPolicy?: string; +}; + +export type FabricChannelConfig = { + centerApiBase?: string; + accounts?: Record; + defaultAccount?: string; +} & FabricAccountConfig; + +type Cfg = { channels?: { fabric?: FabricChannelConfig }; [k: string]: unknown }; + +export type ResolvedFabricAccount = { + accountId: string; + enabled: boolean; + centerApiBase: string; + fabricApiKey: string; + allowFrom: string[]; + dmPolicy: string | undefined; +}; + +const DEFAULT_CENTER = 'http://localhost:7001/api'; + +function section(cfg: Cfg): FabricChannelConfig { + return cfg.channels?.fabric ?? {}; +} + +export function listFabricAccountIds(cfg: Cfg): string[] { + const accts = section(cfg).accounts ?? {}; + const ids = Object.keys(accts); + return ids.length ? ids : ['default']; +} + +export function resolveDefaultFabricAccountId(cfg: Cfg): string { + const s = section(cfg); + if (s.defaultAccount) return s.defaultAccount; + const ids = listFabricAccountIds(cfg); + return ids[0] ?? 'default'; +} + +export function resolveFabricAccount(cfg: Cfg, accountId?: string | null): ResolvedFabricAccount { + const s = section(cfg); + const id = accountId ?? resolveDefaultFabricAccountId(cfg); + const acc = s.accounts?.[id] ?? {}; + const fabricApiKey = (acc.fabricApiKey ?? '').trim(); + const centerApiBase = (acc.centerApiBase ?? s.centerApiBase ?? DEFAULT_CENTER).trim(); + return { + accountId: id, + enabled: acc.enabled !== false && s.enabled !== false, + centerApiBase, + fabricApiKey, + allowFrom: acc.allowFrom ?? s.allowFrom ?? [], + dmPolicy: acc.dmPolicy ?? s.dmPolicy, + }; +} + +export function listEnabledFabricAccounts(cfg: Cfg): ResolvedFabricAccount[] { + return listFabricAccountIds(cfg) + .map((id) => resolveFabricAccount(cfg, id)) + .filter((a) => a.enabled && a.fabricApiKey); +} diff --git a/src/channel.ts b/src/channel.ts index c33a41d..28078ca 100644 --- a/src/channel.ts +++ b/src/channel.ts @@ -1,63 +1,109 @@ +// Fabric channel plugin object (third-party `createChatChannelPlugin` path). +// +// COMPAT NOTE (openclaw v2026.5.7 plugin SDK): +// We depend on these generic SDK shapes — re-verify on openclaw upgrade: +// - createChannelPluginBase requires `capabilities` +// - ChannelSetupAdapter: only `applyAccountConfig` is required +// - outbound attachedResults.sendText returns Omit (so `messageId: string` is required) +// Casts at the createChatChannelPlugin boundary are intentional and +// localized; keep them here so upgrades touch one file. import { createChatChannelPlugin, createChannelPluginBase, -} from 'openclaw/plugin-sdk/channel-core'; -import type { OpenClawConfig } from 'openclaw/plugin-sdk/channel-core'; +} from 'openclaw/plugin-sdk/core'; +import { FabricClient } from './fabric-client.js'; +import { + listFabricAccountIds, + resolveFabricAccount, + resolveDefaultFabricAccountId, + type ResolvedFabricAccount, +} from './accounts.js'; -export type ResolvedFabricAccount = { - accountId: string | null; - centerApiBase: string; - allowFrom: string[]; - dmPolicy: string | undefined; -}; +type AnyCfg = { channels?: { fabric?: unknown }; [k: string]: unknown }; -export function resolveFabricAccount( - cfg: OpenClawConfig, - accountId?: string | null, -): ResolvedFabricAccount { - const section = (cfg.channels as Record)?.['fabric']; - const centerApiBase: string | undefined = section?.centerApiBase; - if (!centerApiBase) throw new Error('fabric: channels.fabric.centerApiBase is required'); - return { - accountId: accountId ?? null, - centerApiBase, - allowFrom: section?.allowFrom ?? [], - dmPolicy: section?.dmSecurity, - }; +// Posts an agent's reply to Fabric. `to` is the Fabric channelId; `accountId` +// is the agentId (= Fabric identity). One auth concept: account apiKey -> +// agent/login -> guild token -> POST message. +async function sendToFabric( + cfg: AnyCfg, + accountId: string | null | undefined, + to: string, + text: string, +): Promise<{ messageId: string }> { + const acc = resolveFabricAccount(cfg as never, accountId); + if (!acc.fabricApiKey) throw new Error(`fabric account ${acc.accountId} has no fabricApiKey`); + const client = new FabricClient(acc.centerApiBase); + const session = await client.agentLogin(acc.fabricApiKey); + // find which guild owns this channel by probing each guild's channel list + for (const g of session.guilds) { + const gt = session.guildAccessTokens.find((t) => t.guildNodeId === g.nodeId)?.token; + if (!gt) continue; + const res = await fetch(`${g.endpoint}/api/channels?guildId=${encodeURIComponent(g.nodeId)}`, { + headers: { authorization: `Bearer ${gt}` }, + }); + const channels = res.ok ? ((await res.json()) as Array<{ id: string }>) : []; + if (channels.some((c) => c.id === to)) { + await client.postMessage(g.endpoint, gt, to, text, session.user.id); + return { messageId: `${to}:${Date.now()}` }; + } + } + // fallback: first guild + const g = session.guilds[0]; + const gt = session.guildAccessTokens.find((t) => t.guildNodeId === g?.nodeId)?.token; + if (g && gt) { + await client.postMessage(g.endpoint, gt, to, text, session.user.id); + return { messageId: `${to}:${Date.now()}` }; + } + throw new Error('fabric: no guild available to deliver'); } -// Outbound is wired by the entry (it needs the identity registry + client to -// post as the right agent). Channel-turn visible replies go through the -// inbound adapter's delivery callback; this object owns config/security only. -export function buildFabricChannelPlugin( - sendText: (params: { accountId?: string | null; to: string; text: string }) => Promise<{ messageId?: string }>, -) { - return createChatChannelPlugin({ - base: createChannelPluginBase({ - id: 'fabric', - setup: { - resolveAccount: resolveFabricAccount, - inspectAccount(cfg, accountId) { - const section = (cfg.channels as Record)?.['fabric']; - const ok = Boolean(section?.centerApiBase); - return { enabled: ok, configured: ok, tokenStatus: ok ? 'available' : 'missing' }; - }, - }, - }), - security: { - dm: { - channelKey: 'fabric', - resolvePolicy: (a) => a.dmPolicy, - resolveAllowFrom: (a) => a.allowFrom, - defaultPolicy: 'allowlist', +export const fabricChannelPlugin = createChatChannelPlugin({ + base: createChannelPluginBase({ + id: 'fabric', + meta: { id: 'fabric', label: 'Fabric', blurb: 'Connect OpenClaw agents to a Fabric guild.' }, + capabilities: { + chatTypes: ['channel', 'group', 'direct'], + reactions: false, + threads: false, + media: false, + nativeCommands: false, + blockStreaming: true, + }, + reload: { configPrefixes: ['channels.fabric'] }, + config: { + listAccountIds: (cfg) => listFabricAccountIds(cfg as never), + resolveAccount: (cfg, accountId) => resolveFabricAccount(cfg as never, accountId), + defaultAccountId: (cfg) => resolveDefaultFabricAccountId(cfg as never), + isConfigured: (account: ResolvedFabricAccount) => Boolean(account.fabricApiKey), + }, + // Minimal setup adapter: Fabric is configured directly under + // channels.fabric.* (no interactive wizard). applyAccountConfig is the + // only required member. + setup: { + applyAccountConfig: ({ cfg }: { cfg: unknown }) => cfg as never, + } as never, + }) as never, + + security: { + dm: { + channelKey: 'fabric', + resolvePolicy: (a: ResolvedFabricAccount) => a.dmPolicy, + resolveAllowFrom: (a: ResolvedFabricAccount) => a.allowFrom, + defaultPolicy: 'allowlist', + }, + }, + + threading: { topLevelReplyToMode: 'channel' }, + + outbound: { + base: {}, + attachedResults: { + channel: 'fabric', + sendText: async (ctx: { accountId?: string | null; to: string; text: string; cfg?: unknown }) => { + const cfg = (ctx.cfg ?? {}) as AnyCfg; + return sendToFabric(cfg, ctx.accountId ?? null, ctx.to, ctx.text); }, }, - // Fabric replies thread by being posted into the same channel. - threading: { topLevelReplyToMode: 'channel' }, - outbound: { - attachedResults: { - sendText: async (params) => sendText(params), - }, - }, - }); -} + } as never, +}); diff --git a/src/inbound.ts b/src/inbound.ts index 14b9736..6863222 100644 --- a/src/inbound.ts +++ b/src/inbound.ts @@ -39,10 +39,15 @@ export class FabricInbound { private readonly client: FabricClient, private readonly identity: IdentityRegistry, private readonly log: Logger, + private readonly accounts: Array<{ agentId: string; fabricApiKey: string }> = [], ) {} async start(): Promise { - for (const entry of this.identity.list()) { + const entries = + this.accounts.length > 0 + ? this.accounts.map((a) => ({ agentId: a.agentId, fabricApiKey: a.fabricApiKey })) + : this.identity.list(); + for (const entry of entries) { try { const session = await this.client.agentLogin(entry.fabricApiKey); this.identity.upsert({