8 Commits

Author SHA1 Message Date
180b717eda feat: add fabric-send-sys-msg tool for system-authored Fabric posts
Thin agent-facing wrapper over the Guild backend's x-fabric-system-key
path (see [[reference_fabric_system_msg_api]]). Posts a message into a
specific channel as the guild sentinel author
(00000000-0000-0000-0000-000000000000), not as the calling agent.

Use case driver: Dialectic recruitment broadcasts. ClawSkills'
`analyze-intel` Step 4 currently posts via `fabric-send-message` which
attributes the message to the proposing agent; that's fine for DM
fallbacks but for announce-channel broadcasts the message should look
like a system lifecycle event, not a personal ping. Without this tool,
the only way to get a system-authored post was the close-sub-discussion
internal path — generic broadcast use cases had no door.

Tool shape mirrors fabric-send-message but:
- Reads channels.fabric.commandsSyncKey from openclaw config; empty →
  ok:false with a configuration error (no silent fallthrough to
  agent-bearer posting).
- Optional `wakeupUserId` plumbs through to the backend's
  emitMessageTargeted path: precise wake one recipient or fully silent
  broadcast (default). For announce-channel broadcasts the silent path
  is right — agents poll/discover, they shouldn't be woken on broadcast.
- Caller doesn't need to be a member of the channel (backend isSystem
  branch skips assertParticipant). Guild membership is still required
  because we resolve guild.endpoint from the agent's session.

Manifest gets the tool name so it surfaces in the agent registry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 23:46:33 +01:00
7f96fffca9 feat: create-sub-discussion / close-sub-discussion + per-role guide injection
Adds a host-driven sub-discussion mechanism for short-lived multi-agent
exchanges where one participant (recruitment interviewee, scoped Q&A
target) has no workflow capability of its own — no Meridian state, no
installed skills, no ego identity, possibly just a placeholder Fabric
account. The host stays procedurally in control; the guest just answers
natural-language questions.

The fundamental shift from `create-discussion-channel` is the guide
plumbing: instead of a single shared guide file posted as the channel's
first message (Discord-era model), each role gets its OWN system-prompt
guide, injected at turn time via `before_prompt_build`. The host can
carry the full procedure; the guest sees a tiny orientation tailored to
"answer my questions, don't try to enter workflows you can't enter".

Three new pieces:

1. SubDiscussionStore (src/sub-discussion-store.ts) — in-memory store
   keyed by sub channelId, mirror-persisted to
   ~/.openclaw/fabric-sub-discussion.json so a gateway restart
   mid-interview doesn't strand both parties with no guide. Carries
   host agentId + userId, guest userIds, host guide text, guest guide
   text, callback (parent) channelId / guildNodeId, createdAt.

2. create-sub-discussion tool (src/tools.ts):
   - Creates a discuss-type Fabric channel with guests as members
     (host is creator → auto-included).
   - Persists a store entry indexed by the new channelId.
   - Sleeps FABRIC_SUB_DISCUSSION_GREETING_DELAY_MS (default 500ms,
     env-overridable) for the backend's channel.joined push to land on
     guest sockets, then posts greetingMsg using the host's own Fabric
     account. Turn rotation's activation rule then puts the first guest
     on the spot with wakeup=true — no race where the host posts before
     the guest's socket subs the channel room.

3. close-sub-discussion tool (src/tools.ts):
   - Host-only (rejects non-host callers by agentId match against the
     store).
   - Posts callbackMsg back into the parent channel using the Guild's
     x-fabric-system-key path so the callback lands as a guild/system-
     authored message rather than the host's personal account.
   - Default wakeupHost=true precisely wakes the host on the parent
     channel so the next workflow step (e.g. recruitment onboard) fires
     without waiting for unrelated traffic.
   - Closes the sub channel and drops the store entry.

Plus a `before_prompt_build` hook (src/sub-discussion-hook.ts) the plugin
registers at startup:

  ctx.channelId  → store.find()  → entry
  identity.findByAgentId(ctx.agentId).fabricUserId
  ─ matches entry.hostUserId   → appendSystemContext: entry.hostGuide
  ─ in     entry.guestUserIds  → appendSystemContext: entry.guestGuide
  ─ neither                    → no injection

Fail-closed on unknown agentId / channelId — we never inject the wrong
guide, only the right one or nothing. Provider-gated to messageProvider
in {empty,'fabric'} so non-fabric triggers (HF wake, exec-event) don't
unintentionally pick up an injection.

Wiring in index.ts threads the new store + config into both
registerFabricTools (so close-sub-discussion can read the system key
from channels.fabric.commandsSyncKey) and registerSubDiscussionHook (so
the prompt hook can resolve agentId → fabricUserId via identity).

Manifest update: openclaw.plugin.json lists `create-sub-discussion`
and `close-sub-discussion` in contracts.tools so the registry surfaces
them to the agent.

Backend prereq: Fabric.Backend.Guild commit 340eed8 restores the
x-fabric-system-key bypass on POST /channels/:id/messages with
wakeupUserId support — close-sub-discussion is a no-op without it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 20:52:01 +01:00
5ff464a055 feat(plugin): fabric-guild-list + fabric-channel-set-purpose tools + purpose on existing tools
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.'
2026-05-23 19:22:10 +01:00
cd36d1b9e2 feat(tools): fabric-send-message + fabric-channel-list + fabric-message-history
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>
2026-05-22 23:11:01 +01:00
8774cfd7cc feat(fabric): coalesce a split agent turn into ONE message (deterministic)
OpenClaw delivers an agent turn whose blocks are text -> thinking/tool
-> text via multiple inbound deliver() calls (a non-text block is a
delivery boundary), so one turn became N Fabric messages.

Fix: buffer deliver() segments per channel (src/coalesce.ts) and flush
them as ONE postMessage at a deterministic boundary — the finally after
dispatchInboundReplyWithBase() resolves, which provably runs only after
every deliver() of the turn (verified: deliver,deliver -> dispatch
returned -> flush). No hooks, no timers, no idle guessing. The
agent_end hook was rejected: it fires BEFORE deliver(). gateway_stop
flushes any leftover; a long safety timeout is a leak-guard only.
channels.fabric.coalesce=false restores raw per-segment posting.

Verified on local openclaw + Fabric with a fake text/thinking/text
model: single trigger -> exactly one merged message.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 22:15:46 +01:00
ab126825ef feat(security): commandsSyncKey is a required channel-config field (Guild C-2)
The slash-command sync secret now comes from
channels.fabric.commandsSyncKey (configSchema marks it required) and
is no longer read from FABRIC_COMMANDS_SYNC_KEY env. command-sync
resolves it from config and threads it into client.syncCommands;
when absent, sync is skipped with a clear warning. README updated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 18:44:25 +01:00
9cb262367e feat: working v1 — full Fabric<->openclaw round-trip verified
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>
2026-05-15 18:24:35 +01:00
9221664428 feat: Fabric channel plugin for OpenClaw (Phase 1, B1)
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>
2026-05-15 17:13:26 +01:00