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>
This commit is contained in:
@@ -20,6 +20,7 @@
|
|||||||
"fabric-canvas",
|
"fabric-canvas",
|
||||||
"fabric-channel",
|
"fabric-channel",
|
||||||
"fabric-send-message",
|
"fabric-send-message",
|
||||||
|
"fabric-send-sys-msg",
|
||||||
"fabric-channel-list",
|
"fabric-channel-list",
|
||||||
"fabric-message-history",
|
"fabric-message-history",
|
||||||
"fabric-guild-list",
|
"fabric-guild-list",
|
||||||
|
|||||||
83
src/tools.ts
83
src/tools.ts
@@ -555,6 +555,89 @@ export function registerFabricTools(
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// ───────────────────────────────────────────────────────────────────
|
||||||
|
// fabric-send-sys-msg: post a system-authored message (author =
|
||||||
|
// sentinel UUID 0000…, not the calling agent) using the Guild's
|
||||||
|
// x-fabric-system-key path. Use for cross-agent broadcasts where you
|
||||||
|
// don't want the message tied to one agent's identity — Dialectic
|
||||||
|
// topic announcements / lifecycle events, host-system advisories,
|
||||||
|
// etc. Caller doesn't need to be a member of the channel (the
|
||||||
|
// backend isSystem branch skips assertParticipant), but must be a
|
||||||
|
// member of the guild (their session resolves the guild endpoint).
|
||||||
|
//
|
||||||
|
// Shared secret: reads channels.fabric.commandsSyncKey (same value
|
||||||
|
// as the guild's FABRIC_BACKEND_GUILD_COMMANDS_SYNC_KEY env). Empty
|
||||||
|
// config → tool returns ok:false with a clear error, no fall-through
|
||||||
|
// to regular agent posting.
|
||||||
|
// ───────────────────────────────────────────────────────────────────
|
||||||
|
api.registerTool((ctx: Ctx) => ({
|
||||||
|
name: 'fabric-send-sys-msg',
|
||||||
|
description:
|
||||||
|
'Send a SYSTEM-AUTHORED message into a Fabric channel (author = guild sentinel, not you). ' +
|
||||||
|
'Use for cross-agent broadcasts that should not be attributed to a single agent — ' +
|
||||||
|
'Dialectic announce-channel topic broadcasts, lifecycle events, system advisories. ' +
|
||||||
|
'Optionally precise-wake one recipient via wakeupUserId; otherwise the message lands ' +
|
||||||
|
'silently in history (no wake).',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
additionalProperties: false,
|
||||||
|
required: ['guildNodeId', 'channelId', 'content'],
|
||||||
|
properties: {
|
||||||
|
guildNodeId: { type: 'string' },
|
||||||
|
channelId: { type: 'string' },
|
||||||
|
content: { type: 'string', description: 'Message body (markdown supported by the renderer).' },
|
||||||
|
wakeupUserId: {
|
||||||
|
type: 'string',
|
||||||
|
description:
|
||||||
|
"Optional: a single Fabric userId to wake with this message (everyone else in the " +
|
||||||
|
'channel sees it but with wakeup=false). Omit for fully silent broadcast.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute: async (
|
||||||
|
_id: string,
|
||||||
|
p: { guildNodeId: string; channelId: string; content: string; wakeupUserId?: string },
|
||||||
|
) => {
|
||||||
|
const agentId = ctx.agentId;
|
||||||
|
if (!agentId) return { ok: false, error: 'no agent context' };
|
||||||
|
const systemKey = resolveCommandsSyncKey(cfg);
|
||||||
|
if (!systemKey) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error:
|
||||||
|
'channels.fabric.commandsSyncKey is not configured — fabric-send-sys-msg needs it for ' +
|
||||||
|
'the x-fabric-system-key header. Configure via openclaw config.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const { guild } = await ctxGuild(agentId, p.guildNodeId);
|
||||||
|
const url = `${guild.endpoint}/api/channels/${encodeURIComponent(p.channelId)}/messages`;
|
||||||
|
const wakeup = typeof p.wakeupUserId === 'string' && p.wakeupUserId.trim()
|
||||||
|
? p.wakeupUserId.trim()
|
||||||
|
: null;
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
'x-fabric-system-key': systemKey,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ content: p.content, wakeupUserId: wakeup }),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const text = await res.text().catch(() => '');
|
||||||
|
return { ok: false, error: `POST ${url} -> ${res.status} ${text}` };
|
||||||
|
}
|
||||||
|
const json = (await res.json().catch(() => null)) as
|
||||||
|
| { messageId?: string; seq?: number; authorUserId?: string }
|
||||||
|
| null;
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
messageId: json?.messageId,
|
||||||
|
seq: json?.seq,
|
||||||
|
authorUserId: json?.authorUserId,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
// -----------------------------------------------------------------
|
// -----------------------------------------------------------------
|
||||||
// fabric-channel-list: enumerate channels the calling agent can see
|
// fabric-channel-list: enumerate channels the calling agent can see
|
||||||
// in a given guild. Backend filters to public channels + channels the
|
// in a given guild. Backend filters to public channels + channels the
|
||||||
|
|||||||
Reference in New Issue
Block a user