Compare commits
2 Commits
152b465e64
...
fix/routin
| Author | SHA1 | Date | |
|---|---|---|---|
| ea713064e1 | |||
| 180b717eda |
@@ -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",
|
||||||
|
|||||||
@@ -51,6 +51,30 @@ type FabricMessage = {
|
|||||||
xType?: string;
|
xType?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Walk cfg.bindings for the entry that ties `agentId` to a fabric account.
|
||||||
|
// Returns the binding's match.accountId (the slot label routing keys on);
|
||||||
|
// returns undefined when the agent has no explicit fabric binding so the
|
||||||
|
// caller can fall back to agentId without changing pre-existing semantics
|
||||||
|
// for agents whose binding accountId == agent_id anyway.
|
||||||
|
function findFabricBindingAccountId(cfg: unknown, agentId: string): string | undefined {
|
||||||
|
const bindings = (cfg as { bindings?: Array<{
|
||||||
|
agentId?: string;
|
||||||
|
match?: { channel?: string; accountId?: string };
|
||||||
|
}> })?.bindings;
|
||||||
|
if (!Array.isArray(bindings)) return undefined;
|
||||||
|
for (const b of bindings) {
|
||||||
|
if (
|
||||||
|
b?.agentId === agentId &&
|
||||||
|
b?.match?.channel === 'fabric' &&
|
||||||
|
typeof b?.match?.accountId === 'string' &&
|
||||||
|
b.match.accountId.length > 0
|
||||||
|
) {
|
||||||
|
return b.match.accountId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export class FabricInbound {
|
export class FabricInbound {
|
||||||
private sockets: Socket[] = [];
|
private sockets: Socket[] = [];
|
||||||
private seen = new Set<string>();
|
private seen = new Set<string>();
|
||||||
@@ -541,10 +565,22 @@ export class FabricInbound {
|
|||||||
// (commands-handlers `isDirectMessage` checks ChatType==='direct')
|
// (commands-handlers `isDirectMessage` checks ChatType==='direct')
|
||||||
// misclassifies the turn.
|
// misclassifies the turn.
|
||||||
const { peerKind, chatType } = fabricPeerRoutingForXType(m.xType);
|
const { peerKind, chatType } = fabricPeerRoutingForXType(m.xType);
|
||||||
|
// resolveAgentRoute needs the *binding* accountId (the channel-side
|
||||||
|
// slot name) — not the openclaw agentId. For most agents the binding
|
||||||
|
// is `{agentId: X, match: {channel: fabric, accountId: X}}` so the
|
||||||
|
// two coincide; but for shared-placeholder cases (e.g. the recruitment
|
||||||
|
// `interviewee` slot bound to multiple agents over its lifetime) the
|
||||||
|
// binding accountId is the slot label ("interviewee", "Neon", …) not
|
||||||
|
// the agent_id. Passing agentId there returned bindings=0 and silently
|
||||||
|
// fell back to `main`, hijacking sub-discussion turns. Look up the
|
||||||
|
// agent's fabric binding accountId here; fall back to agentId when no
|
||||||
|
// explicit binding exists (preserves prior behavior for agents with
|
||||||
|
// no fabric binding declared).
|
||||||
|
const bindingAccountId = findFabricBindingAccountId(this.cfg, agentId) ?? agentId;
|
||||||
const route = core.channel.routing.resolveAgentRoute({
|
const route = core.channel.routing.resolveAgentRoute({
|
||||||
cfg: this.cfg,
|
cfg: this.cfg,
|
||||||
channel: 'fabric',
|
channel: 'fabric',
|
||||||
accountId: agentId,
|
accountId: bindingAccountId,
|
||||||
peer: { kind: peerKind, id: channelId },
|
peer: { kind: peerKind, id: channelId },
|
||||||
});
|
});
|
||||||
const storePath = core.channel.session.resolveStorePath(cfg.session?.store, {
|
const storePath = core.channel.session.resolveStorePath(cfg.session?.store, {
|
||||||
|
|||||||
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