hzhang ea713064e1 fix(routing): resolveAgentRoute uses binding.accountId, not agent_id
`socket.on('message.created', ...)` dispatched with
`accountId: agentId` (the openclaw agent id, e.g. 'analyst2') instead
of the binding's `match.accountId` (the fabric account slot label, e.g.
'interviewee'). For most agents the binding is `{agentId: X, accountId: X}`
so the two coincide and the call works by accident. For shared-placeholder
slots (the recruitment `interviewee` apikey reused across pre-onboard
agents) the slot label is `interviewee` not the agent_id, so the lookup
returns bindings=0 and openclaw core silently falls back to the `main`
agent — which then handles the sub-discussion turn under main's
workspace identity. Symptom: every sub-discussion interview reply
masquerades as the human user's IDENTITY.md text.

Walk cfg.bindings for the entry that ties this agentId to a fabric
account; use its accountId. Fall back to agentId when the agent has
no explicit fabric binding declared (preserves prior behavior for
agents wired before the binding format was uniform).

Verified on prod-t2 recruitment retest 2026-05-31:
  Before: routing log `accountId=analyst2 ... bindings=0`, main session
          ran instead of analyst2.
  After:  `accountId=interviewee ... bindings=1`, analyst2 session ran
          (0 main sessions in sub channel).
2026-05-31 20:32:41 +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%