Pairs with Fabric.Backend.Center@9e1909a which removed the
serviceEndpoint column. The agent-driven recruitment model no longer
needs a service-to-service URL distinction — agents post recruitment
broadcasts directly via fabric-send-message using their normal
session, no backend dials Fabric anymore.
Removed:
- FabricSession.guilds.serviceEndpoint type field
- fabric-guild-list response mapping for serviceEndpoint
- Tool description text that taught agents to plumb it into
dialectic_propose_topic.announce_guild_base_url (that param is
also gone — see Dialectic.OpenclawPlugin@3ce5439).
Kept:
- guild.purpose field (intent-based discovery, still wanted)
- guild.endpoint field (clients dial it directly; unchanged)
FabricSession.guilds type + fabric-guild-list response now carry the
new serviceEndpoint field (added to GuildNode in Fabric.Backend.Center).
The tool description teaches agents the distinction: 'endpoint' is the
client-facing URL (which they themselves use to call the guild from
plugin context), 'serviceEndpoint' is what to plumb into
dialectic_propose_topic's announce_guild_base_url so the dialectic
backend can dial it from inside the deployment.
Fixes bug #2 from the first e2e debate run: agent-supplied
'http://server.t3:7002' wasn't backend-reachable; agent now
supplies 'http://fabric-backend-guild:7002' via serviceEndpoint
and broadcasts actually land.
OpenClaw plugin-sdk's registerTool execute signature is:
execute: async (_id: string, params) => { ... }
Fabric tools were calling it as `(p) => { ... }`, so `p` held the
call id (a string) and the real args were silently dropped onto the
floor. Every tool that read a required field from `p` failed with
the field surfacing as undefined.
fabric-guild-list (just added) appeared to work because all its
properties are optional — `p.nameFilter` and `p.purposeFilter`
both being undefined produced empty filter needles, which let the
unfiltered guild list through. The real bug surfaced the moment
fabric-channel-list (required: guildNodeId) was invoked: the
ctxGuild helper saw `undefined` and reported `agent not a member
of guild undefined`.
Compare dialectic plugin's tools.ts which has always used the
correct `async (_id: string, params) => {...}` shape and worked
end-to-end. Aligning the fabric signature to match.
Verified end-to-end on sim:
- fabric-guild-list returns 1 guild with the purpose set via the
new `cli node set-purpose`
- fabric-channel-list returns 3 channels including a now-populated
`purpose` field on each row
- fabric-channel-set-purpose successfully patches a channel and
the subsequent fabric-channel-list shows the new purpose
Adds two agent-facing tools that close the discoverability loop:
- fabric-guild-list — enumerates guilds the agent belongs to with
name + purpose + status (no api calls beyond the existing agentLogin
response). Optional nameFilter/purposeFilter for narrowing.
- fabric-channel-set-purpose — PATCH /api/channels/:id { purpose }
so agents can backfill or update an existing channel's purpose.
Extends existing tools:
- fabric-channel-list now returns purpose on each row.
- create-{chat,work,report,discussion}-channel accept optional purpose.
FabricClient + FabricSession type changes carry the new field through.
Manifest contracts.tools updated (jiti loader needs both manifest entry
and onStartup activation to register).
Lets workflows that previously needed hardcoded channel ids instead say
'find a guild whose purpose mentions debate, then a channel of x_type
announce whose purpose covers public debate broadcasts.'
Plugin previously had no way for an agent to send text into a specific
channel proactively — outbound went only through the channel-reply
path (responds to the channel that woke the agent). discussion-complete
internally called client.postMessage but only for the close-time
summary, no general-purpose surface.
Three new tools (+ declare existing fabric-canvas / fabric-channel that
were registered but missing from contracts.tools so agents couldn't
see them per the openclaw plugin contract):
* fabric-send-message {guildNodeId, channelId, content}
→ {ok, messageId, seq}
Author = calling agent. Use for ARD broadcasts, follow-ups in a
different channel, etc.
* fabric-channel-list {guildNodeId, nameFilter?, xType?, includeClosed?}
→ {ok, count, channels[]}
Backend filters to public + member channels; nameFilter is client-
side case-insensitive substring; xType / includeClosed apply post-
fetch. Returns id/name/xType/lastSeq so callers can pipe into the
other tools.
* fabric-message-history {guildNodeId, channelId, seqFrom?, seqTo?, limit?}
→ {ok, page, messages[]}
Tail-by-default: omit seqFrom/seqTo and the tool fetches the
channel head from listChannels then asks for [head-limit+1, head].
Limit default 20, max 200. Backend rejects non-participants.
Plus 3 supporting client methods (listChannels, listMessages — both
GET via existing req helper).
contracts.tools updated to declare these 5 (3 new + 2 previously-
silent ones). Verified earlier in sim restart logs: openclaw warned
'plugin tool is undeclared (fabric): fabric-canvas / fabric-channel'
so agents couldn't use them despite registerTool firing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
One tool, three actions backed by FabricClient channelMembers (GET
/channels/:id/members -> [{userId,bypass}]), joinChannel, and new
leaveChannel (POST /channels/:id/leave).
Verified: client-level smoke against the running guild — members
initial=[tester], after join echo2 present, after leave echo2 gone.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- bin/fabric-register.mjs: only AGENT_ID is read from the environment;
--api-key is flag-only (no FABRIC_API_KEY); dropped FABRIC_CENTER_API_BASE
/ FABRIC_IDENTITY_FILE / OPENCLAW_PATH env fallbacks (flags + sensible
defaults; --center still falls back to openclaw.json).
- New fabric-canvas tool (one tool, four actions): read / share / update /
close the channel's single pinned canvas. Backed by FabricClient
get/share/update/removeCanvas (GET/PUT/PATCH/DELETE; empty 2xx body ->
null). update/close are sharer-only server-side.
- README updated.
Verified: client-level smoke against the running guild —
read(empty→null) → share(v1) → read → update(v2) → close(→null) all pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Binding an agent's Fabric API key was an OpenClaw tool; make it a
self-contained Node script installed to ~/.openclaw/bin/fabric-register
instead.
- bin/fabric-register.mjs: no plugin deps; AGENT_ID env wins, else
--agent-id required; --api-key validated via POST /auth/agent/login;
on success upserts ~/.openclaw/fabric-identity.json (format matches
IdentityRegistry). Flags/env for center, identity-file, openclaw-path.
- install.mjs: copy the script to ~/.openclaw/bin (chmod 0755) on
install, remove on uninstall; Next-steps updated.
- tools.ts: drop the fabric-register tool; ctxGuild error now points to
the script / static accounts config.
- README updated.
Verified: missing-id -> exit 2; --agent-id and AGENT_ID both bind and
write a valid identity file; bad key -> 401, no write.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Real channel-turn dispatch (resolveAgentRoute + finalizeInboundContext +
dispatchInboundReplyWithBase), wakeup->drop/dispatch, messaging target
grammar (fabric:<id>) + outbound.sendText, tools use execute/parameters.
Verified live: human msg in Fabric -> wakeup -> openclaw agent runs ->
reply posted back into the Fabric channel as the agent.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
OpenClaw channel plugin: defineChannelPluginEntry + createChatChannelPlugin.
Inbound via channel-turn kernel (wakeup -> admission: true=dispatch,
else drop+recordHistory). One Fabric socket per agent identity in the
plugin runtime (no sidecar). Center API-key agent auth. Tools:
fabric-register, create-{chat,work,report,discussion}-channel,
discussion-complete (post summary + close channel).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>