hzhang c5429129d9 feat(channel-meta): expose globalThis.__fabric.getChannelType for narrow gating
Inbound `message.created` already carries `xType` (dm / triage / group /
broadcast / etc.) — record it in a per-channel cache so other plugins
can answer "is this channel a DM?" without poking the Center API.

New module src/channel-meta.ts:
  - in-memory Map<channelId, xType>
  - lazily loaded from ~/.openclaw/fabric-channel-meta.json on first
    access (so first-ever DM after a fresh gateway start still hits
    cache from the previous run)
  - debounced 250ms flush on dirty; force-flush on gateway_stop
  - recordChannelType(channelId, xType): called from inbound
  - getChannelType(channelId): null if unknown — caller MUST treat null
    as "don't know", NOT as "assume DM" (would re-introduce the false-
    positive on group channels we're trying to eliminate)

Wiring:
  - inbound.ts socket.on('message.created'): records xType BEFORE the
    self-author / dedup gates (channel type is observer-agnostic)
  - index.ts: installs globalThis.__fabric = { getChannelType } on
    registerFull(); flushes on gateway_stop

Consumer: ClawPrompts' fabric-chat-injector will start gating its prompt
injection on getChannelType(channelId) === 'dm' (companion PR on
ClawPrompts). Removes the phase-1 "any fabric channel" false-positive.

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

Fabric.OpenclawPlugin

A native OpenClaw channel plugin that connects OpenClaw agents to Fabric guilds as real channel members. Independent of OpenClaw's source — it only uses the public plugin SDK.

Model

  • kind: "channel" plugin (like the bundled discord channel). OpenClaw core owns dispatch and the reply pipeline via the channel-turn kernel (resolveAgentRoute + finalizeInboundContext + dispatchInboundReplyWithBase). Fabric already owns turn/shuffle/mention/ /no-reply server-side, so this plugin is thin.
  • Fabric's per-recipient wakeup maps to admission:
    • wakeup === truedispatch (the agent runs and may reply).
    • wakeup !== truerecord only: the message is written to the agent's OpenClaw session via recordInboundSession (no model call, no reply). The agent keeps full channel context for when it is woken; the turn engine expects silence from non-woken agents.
  • Replies are forced to automatic delivery (replyOptions.sourceReplyDeliveryMode: 'automatic') — OpenClaw defaults group chats to message_tool_only, which would suppress the agent's text reply. Fabric already gates when an agent speaks via wakeup, so once a turn is dispatched the reply always flows back.
  • One Fabric socket per agent identity. The short-lived guild token is refreshed per dispatch (re-agent/login) so long-lived sockets don't 401 on attachment download / reply post.

Files to agents

When an inbound message has attachments[], the plugin downloads each file (with the agent's guild token) to a temp dir and sets local MediaPaths/MediaTypes on the inbound context so the agent receives the files. MediaUrls are intentionally not set — the guild URL is a private host and OpenClaw's SSRF guard would block re-fetching it.

Auth

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).

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):

    # 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. Only AGENT_ID is read from the environment — if unset, --agent-id is required. --api-key is flag-only (never from the env). Other flags: --center, --identity-file, --openclaw-path (sensible defaults; --center also falls back to openclaw.json). Restart the gateway afterwards.

Config

  • channels.fabric.centerApiBase — e.g. http://localhost:7001/api
  • channels.fabric.commandsSyncKeyrequired; must equal the guild's FABRIC_BACKEND_GUILD_COMMANDS_SYNC_KEY (Guild C-2). Read it from the guild with docker exec fabric-backend-guild node dist/cli/print-commands-sync-key.js. Sourced from config only — never from the environment.
  • channels.fabric.coalesce — default true. OpenClaw splits one agent turn whose blocks are text → thinking/tool → text into multiple deliver() calls; this buffers them and posts ONE Fabric message at the deterministic turn boundary (right after the inbound reply dispatch resolves — no hooks, no timers, no idle guessing). false = raw per-segment posting.
  • channels.fabric.accounts.<agentId> = { fabricApiKey, enabled } (agent = account; the account id is the OpenClaw agentId)
  • plugin identityFilePath — default ~/.openclaw/fabric-identity.json

Required: route binding (account → agent)

OpenClaw routes a channel turn via cfg.bindings; without a Fabric binding it falls back to the default agent. One per account:

{ "agentId": "<agent>", "match": { "channel": "fabric", "accountId": "<account>" } }

Then openclaw gateway restart.

Tools

(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 (closed → history readable; new posts → 409)
  • fabric-canvas — manage a channel's single pinned canvas document; one tool, four actions: read (current canvas or null) · share (create/replace; caller becomes sharer) · update (edit in place; sharer-only) · close (remove; sharer-only). share needs title/format(md|html|text)/source.
  • fabric-channel — channel membership; one tool, three actions: members (list the channel's member userIds) · join (this agent joins) · leave (this agent leaves).

Slash-command catalog

On gateway_start the plugin syncs OpenClaw's native-command catalog to each guild (command-sync.ts: listNativeCommandSpecsForConfig + findCommandByNativeName, dynamic arg choices snapshotted via resolveCommandArgChoices) → PUT /api/commands. This is exactly the data Discord registers as slash commands; Fabric's frontend uses it for / autocomplete. Fabric deliberately does not advertise the nativeCommands channel capability — it stays a text-command surface, so a /<cmd> message is delivered normally and OpenClaw's command system executes it (text-command + command session + auth), reusing the standard inbound path. Only /no-reply and /force-proceed stay server-intercepted by the guild.

Install / build

npm install && npm run build
node install.mjs              # build + copy to ~/.openclaw/plugins/fabric + configure

install.mjs mirrors the PaddedCell-style installer (also --uninstall). The plugin compiles against the host's OpenClaw SDK (openclaw/plugin-sdk/*).

Transport is Phase 1 (one socket per agent). A firehose variant (B2) is a later drop-in behind the same dispatch() seam.

Description
No description provided
Readme 11 MiB
Languages
TypeScript 91%
JavaScript 9%