@clawling/clawchat-plugin-openclaw 2026.10.9-3 → 2026.10.9-4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/host-tools-guard.js +5 -3
- package/dist/src/outbound.js +28 -2
- package/dist/src/runtime.js +1 -1
- package/dist/src/storage.js +112 -13
- package/dist/src/terminal-send.js +69 -12
- package/dist/src/tools.js +203 -21
- package/dist/src/turn-boundary.js +17 -0
- package/package.json +1 -1
- package/src/host-tools-guard.ts +5 -2
- package/src/outbound.ts +39 -3
- package/src/runtime.ts +1 -1
- package/src/storage.ts +134 -15
- package/src/terminal-send.ts +97 -13
- package/src/tools.ts +216 -22
- package/src/turn-boundary.ts +18 -0
package/dist/src/tools.js
CHANGED
|
@@ -8,12 +8,13 @@ import { ClawlingApiError, } from "./api-types.js";
|
|
|
8
8
|
import { mapGateOutcome } from "./gate-outcome.js";
|
|
9
9
|
import { CHANNEL_ID, normalizeOpenclawClawlingAccountId, resolveOpenclawClawlingAccount, } from "./config.js";
|
|
10
10
|
import { clawChatDbPathForStateDir, getClawChatStore, makeOwnerProfilePersister, } from "./storage.js";
|
|
11
|
-
import { sendOpenclawClawlingMentionMessage, sendOpenclawClawlingReaction } from "./outbound.js";
|
|
11
|
+
import { isPlausibleMessageId, sendOpenclawClawlingMentionMessage, sendOpenclawClawlingReaction } from "./outbound.js";
|
|
12
|
+
import { isValidChatId } from "./ws-client.js";
|
|
12
13
|
import { getOpenclawClawlingClient, } from "./runtime.js";
|
|
13
14
|
import { markTerminalClawChatSend, getCurrentTerminalSendScope } from "./terminal-send.js";
|
|
14
15
|
import { editClawChatMemoryBody, readClawChatMemoryFile, resolveClawChatMemoryPath, searchClawChatMemory, writeClawChatMemoryBody, } from "./clawchat-memory.js";
|
|
15
16
|
import { canReadMemory, memoryScopeNote, memoryScopeRefusal, resolveMemoryScope, } from "./memory-scope.js";
|
|
16
|
-
import { clawChatSessionAccountId, NON_OWNER_TOOL_ALLOWLIST } from "./turn-boundary.js";
|
|
17
|
+
import { clawChatSessionAccountId, NON_OWNER_TOOL_ALLOWLIST, normalizeToolChatId } from "./turn-boundary.js";
|
|
17
18
|
import { pullGroupMetadata, pullOwnerMetadata, pullUserMetadata, pushMetadata, updateMetadata, } from "./clawchat-metadata.js";
|
|
18
19
|
import { ClawchatGetAccountProfileSchema, ClawchatGetConversationSchema, ClawchatGetDirectConversationSchema, ClawchatGetMomentSchema, ClawchatGetUserProfileSchema, ClawchatLeaveGroupSchema, ClawchatAddGroupMemberSchema, ClawchatAcceptFriendRequestSchema, ClawchatMemoryEditSchema, ClawchatMemoryReadSchema, ClawchatMemorySearchSchema, ClawchatMemoryWriteSchema, ClawchatMetadataSyncSchema, ClawchatMetadataUpdateSchema, ClawchatCreateMomentCommentSchema, ClawchatCreateMomentSchema, ClawchatDeleteMomentCommentSchema, ClawchatDeleteMomentSchema, ClawchatListAccountFriendsSchema, ClawchatListFriendRequestsSchema, ClawchatListMomentsSchema, ClawchatMentionMessageSchema, ClawchatReactMessageSchema, ClawchatReplyMomentCommentSchema, ClawchatRejectFriendRequestSchema, ClawchatRemoveFriendSchema, ClawchatSearchUsersSchema, ClawchatSendFriendRequestSchema, ClawchatToggleMomentReactionSchema, ClawchatUpdateAccountProfileSchema, ClawchatUploadAvatarImageSchema, ClawchatRegisterAppSchema, ClawchatListAppsSchema, ClawchatUnregisterAppSchema, ClawchatLivewareLoginSchema, ClawchatOrchListAgentsSchema, ClawchatOrchGetAgentSchema, ClawchatOrchSetAgentBehaviorSchema, ClawchatOrchListGroupsSchema, ClawchatOrchGetGroupSchema, ClawchatOrchSetGroupPromptSchema, ClawchatOrchCreateGroupSchema, ClawchatOrchGroupMemberSchema, ClawchatOrchSetGroupAgentSettingsSchema, ClawchatOrchCreateConnectCodeSchema, ClawchatOrchGetConnectCodeSchema, } from "./tools-schema.js";
|
|
19
20
|
const MAX_UPLOAD_BYTES = 20 * 1024 * 1024;
|
|
@@ -91,6 +92,15 @@ export function apiError(err) {
|
|
|
91
92
|
function validationError(message) {
|
|
92
93
|
return jsonResponse({ error: "validation", message });
|
|
93
94
|
}
|
|
95
|
+
function reactionBlockedError(chatId, reason) {
|
|
96
|
+
return jsonResponse({
|
|
97
|
+
error: "send_blocked",
|
|
98
|
+
reason,
|
|
99
|
+
message: reason === "invalid_chat_id"
|
|
100
|
+
? `clawchat-plugin-openclaw: "${chatId}" is not a ClawChat chat id (expected cnv_…); the reaction was not sent`
|
|
101
|
+
: "clawchat-plugin-openclaw: this chat was rejected by the server; the reaction was not sent",
|
|
102
|
+
});
|
|
103
|
+
}
|
|
94
104
|
function genericError(err) {
|
|
95
105
|
return jsonResponse({
|
|
96
106
|
error: "unknown",
|
|
@@ -173,12 +183,18 @@ function planMomentImages(images) {
|
|
|
173
183
|
}
|
|
174
184
|
return { ok: true, entries };
|
|
175
185
|
}
|
|
186
|
+
const MESSAGE_ID_ECHO_LIMIT = 64;
|
|
187
|
+
function notAMessageIdMessage(field, value) {
|
|
188
|
+
const shown = value.length > MESSAGE_ID_ECHO_LIMIT ? `${value.slice(0, MESSAGE_ID_ECHO_LIMIT)}…` : value;
|
|
189
|
+
return `clawchat-plugin-openclaw: ${field} "${shown}" is not a message in this chat; use the real message id from this chat's message metadata (e.g. msg-…), or omit it`;
|
|
190
|
+
}
|
|
176
191
|
function validateMentionMessageParams(params) {
|
|
177
192
|
if (!params || typeof params !== "object") {
|
|
178
193
|
return { ok: false, message: "clawchat-plugin-openclaw: params are required" };
|
|
179
194
|
}
|
|
180
195
|
const p = params;
|
|
181
|
-
|
|
196
|
+
const chatId = normalizeToolChatId(p.chatId);
|
|
197
|
+
if (!chatId) {
|
|
182
198
|
return { ok: false, message: "clawchat-plugin-openclaw: chatId is required" };
|
|
183
199
|
}
|
|
184
200
|
if (p.chatType !== undefined && p.chatType !== "group" && p.chatType !== "direct") {
|
|
@@ -214,11 +230,12 @@ function validateMentionMessageParams(params) {
|
|
|
214
230
|
ok: true,
|
|
215
231
|
params: {
|
|
216
232
|
...p,
|
|
233
|
+
chatId,
|
|
217
234
|
replyToMessageId,
|
|
218
235
|
},
|
|
219
236
|
};
|
|
220
237
|
}
|
|
221
|
-
return { ok: true, params: p };
|
|
238
|
+
return { ok: true, params: { ...p, chatId } };
|
|
222
239
|
}
|
|
223
240
|
/**
|
|
224
241
|
* Account a tool call belongs to, or `null` when the call carries no ClawChat
|
|
@@ -244,6 +261,17 @@ export function resolveOpenclawClawlingToolAccountId(ctx) {
|
|
|
244
261
|
}
|
|
245
262
|
return null;
|
|
246
263
|
}
|
|
264
|
+
/**
|
|
265
|
+
* True when the host says this tool call's run is a ClawChat run: every
|
|
266
|
+
* channel the trusted ctx names (`deliveryContext.channel`, `messageChannel`)
|
|
267
|
+
* is this channel, and at least one is present. Missing or foreign → false.
|
|
268
|
+
*/
|
|
269
|
+
export function isClawChatToolRun(ctx) {
|
|
270
|
+
const channels = [ctx.deliveryContext?.channel, ctx.messageChannel]
|
|
271
|
+
.map((channel) => (typeof channel === "string" ? channel.trim().toLowerCase() : ""))
|
|
272
|
+
.filter(Boolean);
|
|
273
|
+
return channels.length > 0 && channels.every((channel) => channel === CHANNEL_ID);
|
|
274
|
+
}
|
|
247
275
|
// Names this plugin registered through its wrapper below. The host-tool guard
|
|
248
276
|
// lets an allowlisted `clawchat_*` name through only when it is in this set,
|
|
249
277
|
// so another plugin's tool under a lookalike name is not trusted by prefix.
|
|
@@ -306,6 +334,90 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
306
334
|
return null;
|
|
307
335
|
}
|
|
308
336
|
}
|
|
337
|
+
let warnedMessageIdStore = false;
|
|
338
|
+
/**
|
|
339
|
+
* An explicit message id (react target, mention reply) must be one the
|
|
340
|
+
* plugin has stored for this chat; the stored chat id and message id (their
|
|
341
|
+
* real case) are what gets sent. When the store cannot answer, fall back to
|
|
342
|
+
* the message-id shape check rather than block every call, keep the ids as
|
|
343
|
+
* given, and warn once without ids or arguments.
|
|
344
|
+
*/
|
|
345
|
+
function resolveExplicitMessageId(input) {
|
|
346
|
+
let seen = null;
|
|
347
|
+
try {
|
|
348
|
+
const store = resolveStore();
|
|
349
|
+
seen = store?.findSeenMessageInChat
|
|
350
|
+
? store.findSeenMessageInChat({
|
|
351
|
+
platform: "openclaw",
|
|
352
|
+
accountId: input.accountId,
|
|
353
|
+
chatId: input.chatId,
|
|
354
|
+
messageId: input.value,
|
|
355
|
+
})
|
|
356
|
+
: null;
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
seen = null;
|
|
360
|
+
}
|
|
361
|
+
if (seen)
|
|
362
|
+
return { ok: true, chatId: seen.chatId, messageId: seen.messageId };
|
|
363
|
+
if (seen === null) {
|
|
364
|
+
if (!warnedMessageIdStore) {
|
|
365
|
+
warnedMessageIdStore = true;
|
|
366
|
+
api.logger.warn("clawchat-plugin-openclaw: message-id store check unavailable; falling back to the message-id shape check");
|
|
367
|
+
}
|
|
368
|
+
if (isPlausibleMessageId(input.value))
|
|
369
|
+
return { ok: true, chatId: input.chatId, messageId: input.value };
|
|
370
|
+
}
|
|
371
|
+
return { ok: false, error: validationError(notAMessageIdMessage(input.field, input.value)) };
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* The chat id in the case the ledger already stores for it, so a send, its
|
|
375
|
+
* terminal marker and its ledger row all name the chat as the inbound path
|
|
376
|
+
* does (a second case splits the chat for memory scope). The id as given
|
|
377
|
+
* when the ledger has no row for the chat or cannot answer.
|
|
378
|
+
*/
|
|
379
|
+
function storedChatIdCase(accountId, chatId) {
|
|
380
|
+
try {
|
|
381
|
+
const stored = resolveStore()?.findStoredChatId?.({ platform: "openclaw", accountId, chatId });
|
|
382
|
+
return typeof stored === "string" && stored ? stored : chatId;
|
|
383
|
+
}
|
|
384
|
+
catch {
|
|
385
|
+
return chatId;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Best-effort ledger row for a sent mention, so its id can be reacted to or
|
|
390
|
+
* quoted later. Written after the ack; a store failure never affects the send.
|
|
391
|
+
*/
|
|
392
|
+
function recordMentionSent(input) {
|
|
393
|
+
try {
|
|
394
|
+
const store = resolveStore();
|
|
395
|
+
const claimed = store?.claimMessageOnce?.({
|
|
396
|
+
platform: "openclaw",
|
|
397
|
+
accountId: input.accountId,
|
|
398
|
+
kind: "message",
|
|
399
|
+
direction: "outbound",
|
|
400
|
+
eventType: "message.send",
|
|
401
|
+
chatId: input.chatId,
|
|
402
|
+
messageId: input.messageId,
|
|
403
|
+
text: input.text,
|
|
404
|
+
raw: { mode: "tool-mention" },
|
|
405
|
+
});
|
|
406
|
+
if (claimed !== true)
|
|
407
|
+
return;
|
|
408
|
+
store?.markMessageAcknowledged?.({
|
|
409
|
+
accountId: input.accountId,
|
|
410
|
+
kind: "message",
|
|
411
|
+
direction: "outbound",
|
|
412
|
+
messageId: input.messageId,
|
|
413
|
+
protocolMessageId: input.messageId,
|
|
414
|
+
ackedAt: input.acceptedAt ?? Date.now(),
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
// Diagnostics only; the mention has already been sent.
|
|
419
|
+
}
|
|
420
|
+
}
|
|
309
421
|
function detailsError(details) {
|
|
310
422
|
if (!details || typeof details !== "object")
|
|
311
423
|
return null;
|
|
@@ -1001,7 +1113,7 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
1001
1113
|
if (!built.ok)
|
|
1002
1114
|
return built.error;
|
|
1003
1115
|
try {
|
|
1004
|
-
const data = await built.client.getConversation(p.conversationId);
|
|
1116
|
+
const data = await built.client.getConversation(normalizeToolChatId(p.conversationId));
|
|
1005
1117
|
return jsonResponse(data);
|
|
1006
1118
|
}
|
|
1007
1119
|
catch (err) {
|
|
@@ -1063,7 +1175,7 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
1063
1175
|
if (!built.ok)
|
|
1064
1176
|
return built.error;
|
|
1065
1177
|
try {
|
|
1066
|
-
const data = await built.client.leaveConversation(p.conversationId);
|
|
1178
|
+
const data = await built.client.leaveConversation(normalizeToolChatId(p.conversationId));
|
|
1067
1179
|
return jsonResponse(data);
|
|
1068
1180
|
}
|
|
1069
1181
|
catch (err) {
|
|
@@ -1094,7 +1206,7 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
1094
1206
|
if (!built.ok)
|
|
1095
1207
|
return built.error;
|
|
1096
1208
|
try {
|
|
1097
|
-
const data = await built.client.addConversationMember(p.conversationId, p.userId);
|
|
1209
|
+
const data = await built.client.addConversationMember(normalizeToolChatId(p.conversationId), p.userId);
|
|
1098
1210
|
return jsonResponse(data);
|
|
1099
1211
|
}
|
|
1100
1212
|
catch (err) {
|
|
@@ -1131,6 +1243,20 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
1131
1243
|
}
|
|
1132
1244
|
try {
|
|
1133
1245
|
const p = validated.params;
|
|
1246
|
+
let chatId = isValidChatId(p.chatId) ? storedChatIdCase(account.accountId, p.chatId) : p.chatId;
|
|
1247
|
+
let replyToMessageId = p.replyToMessageId;
|
|
1248
|
+
if (replyToMessageId) {
|
|
1249
|
+
const resolved = resolveExplicitMessageId({
|
|
1250
|
+
field: "replyToMessageId",
|
|
1251
|
+
value: replyToMessageId,
|
|
1252
|
+
accountId: account.accountId,
|
|
1253
|
+
chatId,
|
|
1254
|
+
});
|
|
1255
|
+
if (!resolved.ok)
|
|
1256
|
+
return resolved.error;
|
|
1257
|
+
chatId = resolved.chatId;
|
|
1258
|
+
replyToMessageId = resolved.messageId;
|
|
1259
|
+
}
|
|
1134
1260
|
const client = getOpenclawClawlingClient(account.accountId);
|
|
1135
1261
|
if (!client) {
|
|
1136
1262
|
return configError("ClawChat websocket client is not ready");
|
|
@@ -1139,15 +1265,15 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
1139
1265
|
client,
|
|
1140
1266
|
account,
|
|
1141
1267
|
to: {
|
|
1142
|
-
chatId
|
|
1268
|
+
chatId,
|
|
1143
1269
|
chatType: p.chatType ?? "group",
|
|
1144
1270
|
},
|
|
1145
1271
|
...(p.text !== undefined ? { text: p.text } : {}),
|
|
1146
1272
|
mentions: p.mentions,
|
|
1147
|
-
...(
|
|
1273
|
+
...(replyToMessageId
|
|
1148
1274
|
? {
|
|
1149
1275
|
replyCtx: {
|
|
1150
|
-
replyToMessageId
|
|
1276
|
+
replyToMessageId,
|
|
1151
1277
|
},
|
|
1152
1278
|
}
|
|
1153
1279
|
: {}),
|
|
@@ -1155,11 +1281,23 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
1155
1281
|
});
|
|
1156
1282
|
if (!result)
|
|
1157
1283
|
return jsonResponse(result);
|
|
1158
|
-
|
|
1284
|
+
recordMentionSent({
|
|
1159
1285
|
accountId: account.accountId,
|
|
1160
|
-
chatId
|
|
1286
|
+
chatId,
|
|
1161
1287
|
messageId: result.messageId,
|
|
1288
|
+
text: p.text ?? "",
|
|
1289
|
+
...(result.acceptedAt !== undefined ? { acceptedAt: result.acceptedAt } : {}),
|
|
1162
1290
|
});
|
|
1291
|
+
// Same trust rule as the react defaults: only a ClawChat run's
|
|
1292
|
+
// turn may have its follow-up reply suppressed.
|
|
1293
|
+
if (isClawChatToolRun(ctx)) {
|
|
1294
|
+
markTerminalClawChatSend({
|
|
1295
|
+
accountId: account.accountId,
|
|
1296
|
+
chatId,
|
|
1297
|
+
messageId: result.messageId,
|
|
1298
|
+
sessionKey: ctx.sessionKey,
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1163
1301
|
return jsonResponse({
|
|
1164
1302
|
sent: true,
|
|
1165
1303
|
terminal: true,
|
|
@@ -1197,15 +1335,55 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
1197
1335
|
if (!account.configured) {
|
|
1198
1336
|
return configError("clawchat-plugin-openclaw: account is not configured");
|
|
1199
1337
|
}
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1338
|
+
// Defaults come only from this call's own turn: the open turn in
|
|
1339
|
+
// its async context registered for the same session key as the
|
|
1340
|
+
// trusted tool ctx (see terminal-send.ts). Anything else fails as
|
|
1341
|
+
// a validation error asking for explicit ids.
|
|
1342
|
+
// The run must also be a ClawChat run: under a session key shared
|
|
1343
|
+
// with another channel, that channel's turns never register or
|
|
1344
|
+
// supersede a ClawChat turn, so its calls get no defaults.
|
|
1345
|
+
const scope = isClawChatToolRun(ctx)
|
|
1346
|
+
? getCurrentTerminalSendScope({ sessionKey: ctx.sessionKey })
|
|
1347
|
+
: null;
|
|
1348
|
+
// Only a missing or blank chatId defaults. A given one that strips
|
|
1349
|
+
// to nothing (`clawchat-plugin-openclaw:`, `group:`) stays as given
|
|
1350
|
+
// so outbound refuses it as invalid_chat_id instead of it silently
|
|
1351
|
+
// reaching the current turn's chat.
|
|
1352
|
+
const rawChatId = typeof p.chatId === "string" ? p.chatId.trim() : "";
|
|
1353
|
+
let chatId = rawChatId ? normalizeToolChatId(rawChatId) || rawChatId : scope?.chatId;
|
|
1354
|
+
const explicitTargetMessageId = typeof p.targetMessageId === "string" ? p.targetMessageId.trim() : "";
|
|
1355
|
+
// The default target is the current turn's message, so it applies
|
|
1356
|
+
// only to that turn's chat: chatId omitted, or naming the scope's
|
|
1357
|
+
// chat (any case, host target form). The send then goes to the
|
|
1358
|
+
// scope's chat id. In any other chat that id names no message.
|
|
1359
|
+
const defaultsToScopeChat = Boolean(scope)
|
|
1360
|
+
&& (!rawChatId || (chatId ?? "").toLowerCase() === scope.chatId.toLowerCase());
|
|
1361
|
+
let targetMessageId = explicitTargetMessageId || (defaultsToScopeChat ? scope.messageId : undefined);
|
|
1362
|
+
if (!explicitTargetMessageId && defaultsToScopeChat)
|
|
1363
|
+
chatId = scope.chatId;
|
|
1203
1364
|
if (!chatId) {
|
|
1204
1365
|
return validationError("clawchat-plugin-openclaw: chatId is required (no current-turn chat to default from)");
|
|
1205
1366
|
}
|
|
1206
1367
|
if (!targetMessageId) {
|
|
1207
1368
|
return validationError("clawchat-plugin-openclaw: targetMessageId is required (no current-turn message to default from)");
|
|
1208
1369
|
}
|
|
1370
|
+
// Only an explicit id is checked; the current-turn default is the
|
|
1371
|
+
// triggering message's own id. A chat id outbound would refuse is
|
|
1372
|
+
// reported as that refusal first.
|
|
1373
|
+
if (explicitTargetMessageId) {
|
|
1374
|
+
if (!isValidChatId(chatId))
|
|
1375
|
+
return reactionBlockedError(chatId, "invalid_chat_id");
|
|
1376
|
+
const resolved = resolveExplicitMessageId({
|
|
1377
|
+
field: "targetMessageId",
|
|
1378
|
+
value: explicitTargetMessageId,
|
|
1379
|
+
accountId: account.accountId,
|
|
1380
|
+
chatId,
|
|
1381
|
+
});
|
|
1382
|
+
if (!resolved.ok)
|
|
1383
|
+
return resolved.error;
|
|
1384
|
+
chatId = resolved.chatId;
|
|
1385
|
+
targetMessageId = resolved.messageId;
|
|
1386
|
+
}
|
|
1209
1387
|
const client = getOpenclawClawlingClient(account.accountId);
|
|
1210
1388
|
if (!client) {
|
|
1211
1389
|
return configError("ClawChat websocket client is not ready");
|
|
@@ -1213,7 +1391,11 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
1213
1391
|
const emoji = p.emoji.trim();
|
|
1214
1392
|
const removed = p.remove === true;
|
|
1215
1393
|
try {
|
|
1216
|
-
sendOpenclawClawlingReaction({ client, chatId, targetMessageId, emoji, removed, log: api.logger });
|
|
1394
|
+
const sent = sendOpenclawClawlingReaction({ client, chatId, targetMessageId, emoji, removed, log: api.logger });
|
|
1395
|
+
// A locally refused reaction never left the plugin: report the
|
|
1396
|
+
// refusal so the model does not tell the user it reacted.
|
|
1397
|
+
if (!sent.sent)
|
|
1398
|
+
return reactionBlockedError(chatId, sent.reason);
|
|
1217
1399
|
return jsonResponse({ reacted: true, targetMessageId, emoji, removed });
|
|
1218
1400
|
}
|
|
1219
1401
|
catch (err) {
|
|
@@ -1682,7 +1864,7 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
1682
1864
|
async execute(_callId, params) {
|
|
1683
1865
|
return await recordClawchatToolCall(accountId, "clawchat_orchestrate_get_group", params, async () => {
|
|
1684
1866
|
const p = params;
|
|
1685
|
-
return await withClient(accountId, (c) => c.orchGetGroup(p.conversationId));
|
|
1867
|
+
return await withClient(accountId, (c) => c.orchGetGroup(normalizeToolChatId(p.conversationId)));
|
|
1686
1868
|
});
|
|
1687
1869
|
},
|
|
1688
1870
|
};
|
|
@@ -1705,7 +1887,7 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
1705
1887
|
async execute(_callId, params) {
|
|
1706
1888
|
return await recordClawchatToolCall(accountId, "clawchat_orchestrate_set_group_prompt", params, async () => {
|
|
1707
1889
|
const p = params;
|
|
1708
|
-
return await withClient(accountId, (c) => c.orchSetGroupPrompt(p.conversationId, p.description));
|
|
1890
|
+
return await withClient(accountId, (c) => c.orchSetGroupPrompt(normalizeToolChatId(p.conversationId), p.description));
|
|
1709
1891
|
});
|
|
1710
1892
|
},
|
|
1711
1893
|
};
|
|
@@ -1738,7 +1920,7 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
1738
1920
|
async execute(_callId, params) {
|
|
1739
1921
|
return await recordClawchatToolCall(accountId, "clawchat_orchestrate_add_group_member", params, async () => {
|
|
1740
1922
|
const p = params;
|
|
1741
|
-
return await withClient(accountId, (c) => c.orchAddGroupMember(p.conversationId, p.agentId));
|
|
1923
|
+
return await withClient(accountId, (c) => c.orchAddGroupMember(normalizeToolChatId(p.conversationId), p.agentId));
|
|
1742
1924
|
});
|
|
1743
1925
|
},
|
|
1744
1926
|
};
|
|
@@ -1754,7 +1936,7 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
1754
1936
|
async execute(_callId, params) {
|
|
1755
1937
|
return await recordClawchatToolCall(accountId, "clawchat_orchestrate_remove_group_member", params, async () => {
|
|
1756
1938
|
const p = params;
|
|
1757
|
-
return await withClient(accountId, (c) => c.orchRemoveGroupMember(p.conversationId, p.agentId));
|
|
1939
|
+
return await withClient(accountId, (c) => c.orchRemoveGroupMember(normalizeToolChatId(p.conversationId), p.agentId));
|
|
1758
1940
|
});
|
|
1759
1941
|
},
|
|
1760
1942
|
};
|
|
@@ -1772,7 +1954,7 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
1772
1954
|
async execute(_callId, params) {
|
|
1773
1955
|
return await recordClawchatToolCall(accountId, "clawchat_orchestrate_set_group_agent_settings", params, async () => {
|
|
1774
1956
|
const p = params;
|
|
1775
|
-
return await withClient(accountId, (c) => c.orchSetGroupAgentSettings(p.conversationId, p.agentId, {
|
|
1957
|
+
return await withClient(accountId, (c) => c.orchSetGroupAgentSettings(normalizeToolChatId(p.conversationId), p.agentId, {
|
|
1776
1958
|
muted: p.muted,
|
|
1777
1959
|
replyMode: p.replyMode,
|
|
1778
1960
|
batchDelaySeconds: p.batchDelaySeconds,
|
|
@@ -10,6 +10,23 @@ export const NON_OWNER_TOOL_ALLOWLIST = Object.freeze([
|
|
|
10
10
|
"web_search",
|
|
11
11
|
]);
|
|
12
12
|
const norm = (v) => (typeof v === "string" ? v.trim().toLowerCase() : "");
|
|
13
|
+
// `<channel>:` then `direct:`/`group:`, each optional and stripped at most once.
|
|
14
|
+
const TOOL_CHAT_ID_TARGET_PREFIX = new RegExp(`^(?:${CHANNEL_ID}:)?(?:(?:direct|group):)?`, "i");
|
|
15
|
+
/**
|
|
16
|
+
* The bare ClawChat chat id a tool argument names. The host shows the model
|
|
17
|
+
* this chat's `chat_id` as the channel-prefixed origin target
|
|
18
|
+
* (`clawchat-plugin-openclaw:cnv_…` / `clawchat-plugin-openclaw:group:cnv_…`)
|
|
19
|
+
* and a group's label as `group:cnv_…`; models copy either form into tool
|
|
20
|
+
* arguments. Only that form is stripped; case is kept, and whatever remains is
|
|
21
|
+
* still validated by the send path (`isValidChatId`) or the server. Every tool
|
|
22
|
+
* that takes a ClawChat chat id, and the non-owner current-chat check, use
|
|
23
|
+
* this one helper so they agree on which chat an argument names.
|
|
24
|
+
*/
|
|
25
|
+
export function normalizeToolChatId(value) {
|
|
26
|
+
if (typeof value !== "string")
|
|
27
|
+
return "";
|
|
28
|
+
return value.trim().replace(TOOL_CHAT_ID_TARGET_PREFIX, "").trim();
|
|
29
|
+
}
|
|
13
30
|
/**
|
|
14
31
|
* Host session keys (dmScope per-account-channel-peer):
|
|
15
32
|
* agent:<agent>:<channel>:<account>:direct:<chat>
|
package/package.json
CHANGED
package/src/host-tools-guard.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
classifyChat,
|
|
11
11
|
clawChatSessionAccountId,
|
|
12
12
|
NON_OWNER_TOOL_ALLOWLIST,
|
|
13
|
+
normalizeToolChatId,
|
|
13
14
|
parseClawChatSessionKey,
|
|
14
15
|
} from "./turn-boundary.ts";
|
|
15
16
|
import { clawChatDbPathForStateDir, getClawChatStore } from "./storage.ts";
|
|
@@ -76,8 +77,10 @@ export function decideHostToolCall(input: HostToolInput): HostToolDecision {
|
|
|
76
77
|
if (HOST_TOOL_SEARCH_CONTROLS.includes(input.toolName)) return undefined;
|
|
77
78
|
if (ACCOUNT_SCOPED_TOOLS.includes(input.toolName)) {
|
|
78
79
|
// D9: a non-owner turn may only act on its own chat, named explicitly.
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
// The tool resolves the same argument with the same helper, so the chat
|
|
81
|
+
// checked here is the chat the call reaches.
|
|
82
|
+
const target = normalizeToolChatId(input.params.chatId).toLowerCase();
|
|
83
|
+
if (!target || target !== parsed.chatId) {
|
|
81
84
|
return {
|
|
82
85
|
block: true,
|
|
83
86
|
blockReason: input.toolName === "clawchat_react_message"
|
package/src/outbound.ts
CHANGED
|
@@ -814,23 +814,33 @@ export interface SendReactionParams {
|
|
|
814
814
|
log?: LogSink;
|
|
815
815
|
}
|
|
816
816
|
|
|
817
|
+
/**
|
|
818
|
+
* Outcome of {@link sendOpenclawClawlingReaction}. `sent: false` means the
|
|
819
|
+
* frame never left the plugin; callers that report to the model must surface
|
|
820
|
+
* it as a failure, never as a reaction.
|
|
821
|
+
*/
|
|
822
|
+
export type SendReactionResult =
|
|
823
|
+
| { sent: true }
|
|
824
|
+
| { sent: false; reason: "invalid_chat_id" | "chat_rejected_by_server" };
|
|
825
|
+
|
|
817
826
|
/**
|
|
818
827
|
* Emit a Protocol-v2 `message.reaction`. Fire-and-forget: reactions are NOT
|
|
819
828
|
* ack-aligned (the server mints the slot-key message_id, so the ack-by-trace_id
|
|
820
829
|
* path used for message.send/reply does not apply). Matches the mobile client.
|
|
830
|
+
* Local refusals are returned, not thrown; a transport failure still throws.
|
|
821
831
|
*/
|
|
822
|
-
export function sendOpenclawClawlingReaction(params: SendReactionParams):
|
|
832
|
+
export function sendOpenclawClawlingReaction(params: SendReactionParams): SendReactionResult {
|
|
823
833
|
if (!isValidChatId(params.chatId)) {
|
|
824
834
|
params.log?.info?.(
|
|
825
835
|
`clawchat reaction blocked chat_id=${params.chatId} reason=invalid_chat_id`,
|
|
826
836
|
);
|
|
827
|
-
return;
|
|
837
|
+
return { sent: false, reason: "invalid_chat_id" };
|
|
828
838
|
}
|
|
829
839
|
if (isOutboundBlocked(params.client, params.chatId)) {
|
|
830
840
|
params.log?.info?.(
|
|
831
841
|
`clawchat reaction blocked chat_id=${params.chatId} reason=chat_rejected_by_server`,
|
|
832
842
|
);
|
|
833
|
-
return;
|
|
843
|
+
return { sent: false, reason: "chat_rejected_by_server" };
|
|
834
844
|
}
|
|
835
845
|
const payload: MessageReactionPayload = {
|
|
836
846
|
target_message_id: params.targetMessageId,
|
|
@@ -849,6 +859,7 @@ export function sendOpenclawClawlingReaction(params: SendReactionParams): void {
|
|
|
849
859
|
params.log?.info?.(
|
|
850
860
|
`clawchat reaction sent chat_id=${params.chatId} target=${params.targetMessageId} emoji=${params.emoji} removed=${params.removed}`,
|
|
851
861
|
);
|
|
862
|
+
return { sent: true };
|
|
852
863
|
}
|
|
853
864
|
|
|
854
865
|
type OutboundClaimStore = Pick<ClawChatStore, "claimMessageOnce" | "markMessageAcknowledged">;
|
|
@@ -875,6 +886,31 @@ function ulid(now: number = Date.now()): string {
|
|
|
875
886
|
return out.join("");
|
|
876
887
|
}
|
|
877
888
|
|
|
889
|
+
// Every message id shape a ClawChat message can carry. The server preserves a
|
|
890
|
+
// client-supplied id verbatim and never parses it, so this is deliberately a
|
|
891
|
+
// plausibility check, not the mint contract:
|
|
892
|
+
// - `msg-<ULID>`: clients (this plugin, mobile, hermes) and the server's own
|
|
893
|
+
// fallback mint; `msg_…` / short `msg-…` forms are kept for older ids;
|
|
894
|
+
// - `sys-<ULID>` / `sys-<request_id>`: server system injections;
|
|
895
|
+
// - a bare ULID: the server's user-message publisher;
|
|
896
|
+
// - `clawchat-plugin-openclaw-…`: this plugin's synthetic turn ids.
|
|
897
|
+
// UUIDs are not accepted: no ClawChat message id the model sees has that
|
|
898
|
+
// shape, while host transcript ids (e.g. from `sessions_search`) do.
|
|
899
|
+
const PREFIXED_MESSAGE_ID = /^(?:msg[-_]|sys-|clawchat-plugin-openclaw-)\S+$/i;
|
|
900
|
+
const BARE_ULID = /^[0-9A-HJKMNP-TV-Z]{26}$/i;
|
|
901
|
+
|
|
902
|
+
/**
|
|
903
|
+
* Whether `id` can be a real ClawChat message id. Rejects what a model makes
|
|
904
|
+
* up when it has no id (a label like `B`, `msg`, prose); the server only checks
|
|
905
|
+
* that a reaction target is non-empty, so such a value would "succeed" on no
|
|
906
|
+
* message. Callers trim first. Tools use this only as the fallback when the
|
|
907
|
+
* plugin store cannot answer whether it has seen the id in the chat.
|
|
908
|
+
*/
|
|
909
|
+
export function isPlausibleMessageId(id: unknown): id is string {
|
|
910
|
+
if (typeof id !== "string") return false;
|
|
911
|
+
return PREFIXED_MESSAGE_ID.test(id) || BARE_ULID.test(id);
|
|
912
|
+
}
|
|
913
|
+
|
|
878
914
|
/** Client-minted message id: `msg-` + ULID (protocol §7.6 format contract). */
|
|
879
915
|
export function mintMessageId(): string {
|
|
880
916
|
return `msg-${ulid()}`;
|
package/src/runtime.ts
CHANGED
|
@@ -3574,7 +3574,7 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
3574
3574
|
dispatcher,
|
|
3575
3575
|
onSettled: () => markDispatchIdle(),
|
|
3576
3576
|
run: () =>
|
|
3577
|
-
runWithTerminalClawChatSendScope(terminalSendScopeId, () =>
|
|
3577
|
+
runWithTerminalClawChatSendScope({ scopeId: terminalSendScopeId, sessionKey: resolvedSessionKey }, () =>
|
|
3578
3578
|
rt.reply.dispatchReplyFromConfig({
|
|
3579
3579
|
ctx: ctxPayload,
|
|
3580
3580
|
cfg: dispatchCfg,
|