@shadowob/openclaw-shadowob 1.1.3 → 1.1.5

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.
@@ -22,6 +22,9 @@ function mentionTargetsBot(params) {
22
22
  return mention.username?.toLowerCase() === botUsername;
23
23
  });
24
24
  }
25
+ function mentionsTargetServerApp(mentions) {
26
+ return mentions.some((mention) => mention.kind === "app" && (mention.appKey || mention.targetId));
27
+ }
25
28
  function formatShadowMentionsForAgent(mentions) {
26
29
  if (mentions.length === 0) return "";
27
30
  const lines = mentions.map((mention) => {
@@ -32,6 +35,9 @@ function formatShadowMentionsForAgent(mentions) {
32
35
  if (mention.kind === "server") {
33
36
  return `- ${label} [server] serverId=${mention.serverId ?? mention.targetId} slug=${mention.serverSlug ?? ""}`;
34
37
  }
38
+ if (mention.kind === "app") {
39
+ return `- ${label} [server-app] appKey=${mention.appKey ?? mention.targetId} appId=${mention.appId ?? mention.targetId} serverId=${mention.serverId ?? ""} server=${mention.serverName ?? ""}`;
40
+ }
35
41
  if (mention.kind === "user" || mention.kind === "buddy") {
36
42
  return `- ${label} [${mention.kind}] userId=${mention.userId ?? mention.targetId} username=${mention.username ?? ""}`;
37
43
  }
@@ -40,8 +46,9 @@ function formatShadowMentionsForAgent(mentions) {
40
46
  return [
41
47
  "Shadow mentions:",
42
48
  ...lines,
43
- "To mention a Shadow entity in a reply, write its visible handle (for example @username or #channel); Shadow will resolve it before delivery."
44
- ].join("\n");
49
+ "To mention a Shadow entity in a reply, write its visible handle (for example @username or #channel); Shadow will resolve it before delivery.",
50
+ mentionsTargetServerApp(mentions) ? 'If a server app is mentioned, operate it through the Shadow CLI only: first run `shadowob app discover --server "<serverId-or-slug>" --json`, then run `shadowob app call "<appKey>" <command> --server "<serverId-or-slug>" --json-input \'<raw-command-input-json>\' --json`. Do not use curl, fetch, raw HTTP routes, or the JavaScript SDK for server-app commands. Use the mentioned appKey/serverId; do not ask the user to describe the CLI path.' : ""
51
+ ].filter(Boolean).join("\n");
45
52
  }
46
53
  function mentionContextFields(mentions) {
47
54
  if (mentions.length === 0) return {};
@@ -66,6 +73,14 @@ function mentionContextFields(mentions) {
66
73
  serverId: mention.serverId ?? mention.targetId,
67
74
  serverSlug: mention.serverSlug,
68
75
  serverName: mention.serverName
76
+ })),
77
+ MentionedApps: mentions.filter((mention) => mention.kind === "app").map((mention) => ({
78
+ appId: mention.appId ?? mention.targetId,
79
+ appKey: mention.appKey,
80
+ appName: mention.appName,
81
+ serverId: mention.serverId,
82
+ serverSlug: mention.serverSlug,
83
+ serverName: mention.serverName
69
84
  }))
70
85
  };
71
86
  }
@@ -162,6 +177,8 @@ async function buildInteractiveResponseContext(params) {
162
177
  const responsePrompt = sourceInteractive && typeof sourceInteractive === "object" && !Array.isArray(sourceInteractive) ? sourceInteractive.responsePrompt : void 0;
163
178
  const lines = [
164
179
  "Shadow interactive response received.",
180
+ "Use the submitted values once. Do not separately restate or grade the submitted form unless the source command explicitly asks for an evaluation.",
181
+ "If the next step is another Shadow interactive dialog, send that dialog only and do not add a separate normal text reply for the same step.",
165
182
  `Source message: ${source?.content ?? "(unavailable)"}`,
166
183
  typeof sourcePrompt === "string" && sourcePrompt.trim() ? `Source prompt: ${sourcePrompt.trim()}` : "",
167
184
  typeof responsePrompt === "string" && responsePrompt.trim() ? `Follow-up instruction: ${responsePrompt.trim()}` : "",
@@ -281,8 +298,10 @@ async function resolveShadowInboundMediaContext(params) {
281
298
  }
282
299
 
283
300
  // src/monitor/preflight.ts
284
- function escapeRegex(value) {
285
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
301
+ function normalizeTriggerUserIds(policyConfig) {
302
+ const value = policyConfig?.allowedTriggerUserIds ?? policyConfig?.triggerUserIds;
303
+ if (!Array.isArray(value)) return null;
304
+ return value.filter((item) => typeof item === "string" && item.length > 0);
286
305
  }
287
306
  function evaluateShadowMessagePreflight(params) {
288
307
  const { message, botUserId, botUsername, channelPolicies, runtime } = params;
@@ -292,7 +311,6 @@ function evaluateShadowMessagePreflight(params) {
292
311
  }
293
312
  const policy = channelPolicies.get(message.channelId);
294
313
  const policyConfig = policy?.config;
295
- const structuredMentions = getShadowMessageMentions(message);
296
314
  let isProcessingBuddyMessage = false;
297
315
  if (message.author?.isBot) {
298
316
  if (!policyConfig?.replyToBuddy) {
@@ -347,82 +365,22 @@ function evaluateShadowMessagePreflight(params) {
347
365
  reason: `[msg] Policy blocks reply for channel ${message.channelId}, skipping (${message.id})`
348
366
  };
349
367
  }
350
- let wasMentionedExplicitly = false;
351
- if (policy?.mentionOnly) {
352
- const mentionRegex = new RegExp(`@${escapeRegex(botUsername)}(?:\\s|$)`, "i");
353
- wasMentionedExplicitly = mentionTargetsBot({ mentions: structuredMentions, botUserId, botUsername }) || mentionRegex.test(message.content);
354
- if (!wasMentionedExplicitly) {
355
- return {
356
- ok: false,
357
- reason: `[msg] mentionOnly policy \u2014 no @${botUsername} mention found, skipping (${message.id})`
358
- };
359
- }
360
- runtime.log?.(
361
- `[msg] mentionOnly policy \u2014 @${botUsername} mentioned, processing (${message.id})`
362
- );
363
- }
364
- if (policyConfig?.replyToUsers?.length) {
365
- const allowedUsers = policyConfig.replyToUsers.map((u) => u.toLowerCase());
366
- const senderUser = (message.author?.username ?? "").toLowerCase();
367
- if (!allowedUsers.includes(senderUser)) {
368
- return {
369
- ok: false,
370
- reason: `[msg] replyToUsers policy \u2014 sender "${senderUser}" not in allowed list, skipping (${message.id})`
371
- };
372
- }
373
- }
374
- if (policyConfig?.keywords?.length) {
375
- const lowerContent = message.content.toLowerCase();
376
- const matched = policyConfig.keywords.some((kw) => lowerContent.includes(kw.toLowerCase()));
377
- if (!matched) {
378
- return {
379
- ok: false,
380
- reason: `[msg] keywords policy \u2014 no matching keyword found, skipping (${message.id})`
381
- };
382
- }
383
- runtime.log?.(`[msg] keywords policy \u2014 keyword matched, processing (${message.id})`);
368
+ const triggerUserIds = normalizeTriggerUserIds(policyConfig);
369
+ if (triggerUserIds && !triggerUserIds.includes(message.authorId)) {
370
+ return {
371
+ ok: false,
372
+ reason: `[msg] Sender ${senderLabel} is not the Buddy owner or active tenant, skipping (${message.id})`
373
+ };
384
374
  }
385
- const smartReplyEnabled = policyConfig?.smartReply !== false;
386
- if (smartReplyEnabled && !isProcessingBuddyMessage && !wasMentionedExplicitly) {
387
- const mentionPattern = /@([a-zA-Z0-9_\-\u4e00-\u9fa5]+)/g;
388
- const userMentions = structuredMentions.filter(
389
- (mention) => mention.kind === "user" || mention.kind === "buddy"
390
- );
391
- const allMentions = structuredMentions.length > 0 ? [] : message.content.match(mentionPattern) || [];
392
- const structuredSelfMentioned = mentionTargetsBot({
393
- mentions: userMentions,
394
- botUserId,
395
- botUsername
396
- });
397
- if (userMentions.length > 0 && !structuredSelfMentioned) {
398
- return {
399
- ok: false,
400
- reason: `[msg] Smart reply: message mentions other users but not @${botUsername}, skipping (${message.id})`
401
- };
402
- }
403
- const mentionsWithoutSelf = userMentions.length > 0 ? [] : allMentions.filter((m) => {
404
- const mentionedUser = m.slice(1).toLowerCase();
405
- return mentionedUser !== botUsername.toLowerCase();
406
- });
407
- if (userMentions.length === 0 && allMentions.length > 0 && mentionsWithoutSelf.length === allMentions.length) {
408
- return {
409
- ok: false,
410
- reason: `[msg] Smart reply: message @mentions others (${allMentions.join(", ")}) but not @${botUsername}, skipping (${message.id})`
411
- };
412
- }
413
- const replyToData = message.replyTo;
414
- if (replyToData?.authorId && replyToData.authorId !== botUserId) {
415
- const selfMentioned = allMentions.some((m) => {
416
- const mentionedUser = m.slice(1).toLowerCase();
417
- return mentionedUser === botUsername.toLowerCase();
418
- }) || structuredSelfMentioned;
419
- if (!selfMentioned) {
420
- return {
421
- ok: false,
422
- reason: `[msg] Smart reply: message is a reply to another user (${replyToData.authorId}), not this Buddy, skipping (${message.id})`
423
- };
424
- }
425
- }
375
+ const structuredMentions = getShadowMessageMentions(message);
376
+ const escapedBotUsername = botUsername.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
377
+ const mentionRegex = new RegExp(`@${escapedBotUsername}(?:\\s|$)`, "i");
378
+ const wasMentionedExplicitly = mentionTargetsBot({ mentions: structuredMentions, botUserId, botUsername }) || mentionsTargetServerApp(structuredMentions) || mentionRegex.test(message.content);
379
+ if (policy?.mentionOnly && !wasMentionedExplicitly) {
380
+ return {
381
+ ok: false,
382
+ reason: `[msg] Policy requires mention for channel ${message.channelId}, skipping (${message.id})`
383
+ };
426
384
  }
427
385
  return {
428
386
  ok: true,
@@ -602,6 +560,7 @@ function resolveSessionStore(cfg) {
602
560
  // src/monitor/slash-commands.ts
603
561
  import fsPromises2 from "fs/promises";
604
562
  var SLASH_COMMAND_RE = /^\/([a-zA-Z][a-zA-Z0-9._-]{0,63})(?:\s+([\s\S]*))?$/;
563
+ var DEFAULT_SLASH_COMMANDS_PATH = "/etc/shadowob/slash-commands.json";
605
564
  function normalizeSlashCommandName(value) {
606
565
  if (typeof value !== "string") return null;
607
566
  const name = value.trim().replace(/^\/+/, "");
@@ -700,6 +659,7 @@ function normalizeShadowSlashCommands(input) {
700
659
  )
701
660
  ].filter((alias) => alias.toLowerCase() !== key) : void 0;
702
661
  const interaction = normalizeSlashInteraction(record.interaction);
662
+ const dispatch = readString(record.dispatch, 40);
703
663
  commands.push({
704
664
  name,
705
665
  ...typeof record.description === "string" && record.description.trim() ? { description: record.description.trim().slice(0, 240) } : {},
@@ -707,16 +667,24 @@ function normalizeShadowSlashCommands(input) {
707
667
  ...typeof record.packId === "string" && record.packId.trim() ? { packId: record.packId.trim().slice(0, 80) } : {},
708
668
  ...typeof record.sourcePath === "string" && record.sourcePath.trim() ? { sourcePath: record.sourcePath.trim().slice(0, 500) } : {},
709
669
  ...typeof record.body === "string" && record.body.trim() ? { body: record.body.trim().slice(0, 2e4) } : {},
670
+ ...dispatch === "agent" || dispatch === "passthrough" ? { dispatch } : {},
710
671
  ...interaction ? { interaction } : {}
711
672
  });
712
673
  }
713
674
  return commands.slice(0, 200);
714
675
  }
715
676
  function toPublicSlashCommands(commands) {
716
- return commands.map(({ body: _body, ...command }) => command);
677
+ return commands.map(({ body: _body, dispatch: _dispatch, ...command }) => command);
717
678
  }
718
- async function loadLocalSlashCommands(runtime) {
719
- const indexPath = process.env.SHADOW_SLASH_COMMANDS_PATH;
679
+ async function fileExists(path) {
680
+ try {
681
+ await fsPromises2.access(path);
682
+ return true;
683
+ } catch {
684
+ return false;
685
+ }
686
+ }
687
+ async function loadSlashCommandFile(indexPath, runtime) {
720
688
  if (!indexPath) return [];
721
689
  try {
722
690
  const raw = await fsPromises2.readFile(indexPath, "utf-8");
@@ -728,6 +696,73 @@ async function loadLocalSlashCommands(runtime) {
728
696
  return [];
729
697
  }
730
698
  }
699
+ function logDuplicateSlashCommands(sources, runtime) {
700
+ const owners = /* @__PURE__ */ new Map();
701
+ for (const source of sources) {
702
+ for (const command of source.commands) {
703
+ const key = command.name.toLowerCase();
704
+ const existingPath = owners.get(key);
705
+ if (existingPath) {
706
+ runtime.log?.(
707
+ `[slash] Ignoring duplicate command /${command.name} from ${source.path}; already defined by ${existingPath}`
708
+ );
709
+ continue;
710
+ }
711
+ owners.set(key, source.path);
712
+ }
713
+ }
714
+ }
715
+ async function runtimeExtensionSlashCommandPaths(runtime) {
716
+ const candidates = [
717
+ process.env.SHADOW_RUNTIME_EXTENSIONS_PATH,
718
+ process.env.OPENCLAW_RUNTIME_EXTENSIONS_PATH,
719
+ "/etc/shadowob/runtime-extensions.json",
720
+ "/etc/openclaw/runtime-extensions.json"
721
+ ].filter((path) => Boolean(path));
722
+ const paths = [];
723
+ for (const manifestPath of [...new Set(candidates)]) {
724
+ if (!await fileExists(manifestPath)) continue;
725
+ try {
726
+ const raw = await fsPromises2.readFile(manifestPath, "utf-8");
727
+ const manifest = JSON.parse(raw);
728
+ const artifacts = Array.isArray(manifest.artifacts) ? manifest.artifacts : [];
729
+ for (const artifact of artifacts) {
730
+ if (!artifact || typeof artifact !== "object" || Array.isArray(artifact)) continue;
731
+ const record = artifact;
732
+ if (record.kind === "shadow.slashCommands" && typeof record.path === "string") {
733
+ paths.push(record.path);
734
+ }
735
+ }
736
+ } catch (err) {
737
+ runtime.error?.(`[slash] Failed to read runtime extensions ${manifestPath}: ${String(err)}`);
738
+ }
739
+ }
740
+ return paths;
741
+ }
742
+ async function loadShadowSlashCommands(runtime) {
743
+ const defaultIndexPath = process.env.SHADOW_DEFAULT_SLASH_COMMANDS_PATH || DEFAULT_SLASH_COMMANDS_PATH;
744
+ const paths = [
745
+ defaultIndexPath,
746
+ process.env.SHADOW_SLASH_COMMANDS_PATH,
747
+ ...await runtimeExtensionSlashCommandPaths(runtime)
748
+ ].filter((path) => Boolean(path));
749
+ const seenPaths = [...new Set(paths)];
750
+ const existingPaths = (await Promise.all(seenPaths.map(async (path) => await fileExists(path) ? path : null))).filter((path) => Boolean(path));
751
+ const sources = await Promise.all(
752
+ existingPaths.map(async (path) => ({
753
+ path,
754
+ commands: await loadSlashCommandFile(path, runtime)
755
+ }))
756
+ );
757
+ logDuplicateSlashCommands(sources, runtime);
758
+ const merged = normalizeShadowSlashCommands(sources.flatMap((source) => source.commands));
759
+ if (existingPaths.length > 1) {
760
+ runtime.log?.(
761
+ `[slash] Merged ${merged.length} slash command(s) from ${existingPaths.length} source(s)`
762
+ );
763
+ }
764
+ return merged;
765
+ }
731
766
  async function registerAgentSlashCommands(params) {
732
767
  const baseUrl = params.account.serverUrl.replace(/\/api\/?$/, "").replace(/\/$/, "");
733
768
  const response = await fetch(`${baseUrl}/api/agents/${params.agentId}/slash-commands`, {
@@ -1148,6 +1183,66 @@ function buildChannelContextForAgent(info, channelId) {
1148
1183
  `Shadow channel id: ${channelId}`
1149
1184
  ].join("\n");
1150
1185
  }
1186
+ function normalizeStringList(value) {
1187
+ if (!Array.isArray(value)) return [];
1188
+ return value.filter((item) => typeof item === "string" && item.trim().length > 0);
1189
+ }
1190
+ function isSenderCommandAuthorized(policyConfig, senderId) {
1191
+ const triggerUserIds = normalizeStringList(
1192
+ policyConfig?.allowedTriggerUserIds ?? policyConfig?.triggerUserIds
1193
+ );
1194
+ if (triggerUserIds.length > 0) return triggerUserIds.includes(senderId);
1195
+ const ownerId = typeof policyConfig?.ownerId === "string" ? policyConfig.ownerId.trim() : "";
1196
+ if (ownerId && ownerId === senderId) return true;
1197
+ const activeTenantIds = normalizeStringList(policyConfig?.activeTenantIds);
1198
+ return activeTenantIds.includes(senderId);
1199
+ }
1200
+ function resolveOwnerAllowFrom(policyConfig) {
1201
+ const ownerId = typeof policyConfig?.ownerId === "string" ? policyConfig.ownerId.trim() : "";
1202
+ return ownerId ? [ownerId] : void 0;
1203
+ }
1204
+ async function buildMentionedServerAppSkillsContext(params) {
1205
+ const appRefs = /* @__PURE__ */ new Map();
1206
+ for (const mention of params.mentions) {
1207
+ if (mention.kind !== "app") continue;
1208
+ const appKey = mention.appKey ?? mention.targetId;
1209
+ const server = mention.serverId ?? mention.serverSlug ?? params.serverInfo?.serverId;
1210
+ if (!appKey || !server) continue;
1211
+ appRefs.set(`${server}:${appKey}`, {
1212
+ appKey,
1213
+ server,
1214
+ label: mention.label || mention.sourceToken || mention.token || appKey
1215
+ });
1216
+ }
1217
+ if (appRefs.size === 0) return "";
1218
+ const documents = await Promise.all(
1219
+ Array.from(appRefs.values()).map(async (ref) => {
1220
+ try {
1221
+ const skill = await params.client.getServerAppSkills(ref.server, ref.appKey);
1222
+ return [
1223
+ `## ${ref.label}`,
1224
+ `Server reference: ${ref.server}`,
1225
+ `App key: ${ref.appKey}`,
1226
+ "",
1227
+ skill.markdown
1228
+ ].join("\n");
1229
+ } catch (err) {
1230
+ params.runtime.error?.(
1231
+ `[server-app] Failed loading skills for ${ref.appKey} on ${ref.server}: ${String(err)}`
1232
+ );
1233
+ return "";
1234
+ }
1235
+ })
1236
+ );
1237
+ const loaded = documents.filter(Boolean);
1238
+ if (loaded.length === 0) return "";
1239
+ return [
1240
+ "Injected Shadow Server App Skills:",
1241
+ "These instructions are authoritative for mentioned server apps in this message. Use the Shadow CLI path described below so Shadow can bind identity, app grants, and policy.",
1242
+ "",
1243
+ ...loaded
1244
+ ].join("\n");
1245
+ }
1151
1246
  async function processShadowMessage(params) {
1152
1247
  const {
1153
1248
  message,
@@ -1209,6 +1304,7 @@ async function processShadowMessage(params) {
1209
1304
  slashCommands
1210
1305
  });
1211
1306
  const slashCommandMatch = matchShadowSlashCommand(cleanBody, slashCommands);
1307
+ const slashCommandPassThrough = slashCommandMatch?.command.dispatch === "passthrough";
1212
1308
  if (slashCommandMatch) {
1213
1309
  runtime.log?.(
1214
1310
  `[slash] Matched /${slashCommandMatch.invokedName} -> /${slashCommandMatch.command.name}`
@@ -1231,17 +1327,22 @@ async function processShadowMessage(params) {
1231
1327
  });
1232
1328
  return;
1233
1329
  }
1234
- const baseBodyForAgent = slashCommandMatch ? formatSlashCommandPrompt(cleanBody, slashCommandMatch) : cleanBody;
1330
+ const baseBodyForAgent = slashCommandMatch && !slashCommandPassThrough ? formatSlashCommandPrompt(cleanBody, slashCommandMatch) : cleanBody;
1331
+ const commandBody = slashCommandPassThrough ? cleanBody : slashCommandMatch?.args ?? cleanBody;
1332
+ const ownerAllowFrom = resolveOwnerAllowFrom(preflight.policyConfig);
1235
1333
  const structuredMentions = getShadowMessageMentions(message);
1236
1334
  const mentionContext = formatShadowMentionsForAgent(structuredMentions);
1237
1335
  const serverInfo = channelServerMap.get(channelId);
1238
1336
  const channelLabel = serverInfo ? `#${serverInfo.channelName}` : `channel:${channelId}`;
1239
1337
  const conversationLabel = serverInfo ? `${serverInfo.serverName} ${channelLabel}` : peerId;
1240
- const messageBodyForAgent = interactiveResponseContext.text ? `${interactiveResponseContext.text}
1241
-
1242
- User message:
1243
- ${baseBodyForAgent}` : baseBodyForAgent;
1338
+ const messageBodyForAgent = interactiveResponseContext.text || baseBodyForAgent;
1244
1339
  const client = new ShadowClient2(account.serverUrl, account.token);
1340
+ const serverAppSkillsContext = await buildMentionedServerAppSkillsContext({
1341
+ mentions: structuredMentions,
1342
+ client,
1343
+ serverInfo,
1344
+ runtime
1345
+ });
1245
1346
  const viewerCommerceContext = await buildCommerceViewerContextForAgent({
1246
1347
  account,
1247
1348
  client,
@@ -1252,6 +1353,7 @@ ${baseBodyForAgent}` : baseBodyForAgent;
1252
1353
  buildCommerceContextForAgent(account),
1253
1354
  viewerCommerceContext,
1254
1355
  mentionContext,
1356
+ serverAppSkillsContext,
1255
1357
  messageBodyForAgent
1256
1358
  ].filter(Boolean).join("\n\n");
1257
1359
  const body = core.channel.reply.formatAgentEnvelope({
@@ -1263,12 +1365,15 @@ ${baseBodyForAgent}` : baseBodyForAgent;
1263
1365
  });
1264
1366
  const escapedBotUsername = botUsername.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1265
1367
  const mentionRegex = new RegExp(`@${escapedBotUsername}(?:\\s|$)`, "i");
1266
- const wasMentioned = mentionTargetsBot({ mentions: structuredMentions, botUserId, botUsername }) || mentionRegex.test(message.content);
1368
+ const wasMentioned = mentionTargetsBot({ mentions: structuredMentions, botUserId, botUsername }) || mentionsTargetServerApp(structuredMentions) || Boolean(slashCommandMatch) || mentionRegex.test(message.content);
1267
1369
  const ctxPayload = core.channel.reply.finalizeInboundContext({
1268
1370
  Body: body,
1269
1371
  BodyForAgent: bodyForAgent,
1270
1372
  RawBody: rawBody,
1271
- CommandBody: slashCommandMatch?.args ?? cleanBody,
1373
+ CommandBody: commandBody,
1374
+ BodyForCommands: commandBody,
1375
+ CommandAuthorized: isSenderCommandAuthorized(preflight.policyConfig, senderId),
1376
+ CommandSource: "text",
1272
1377
  From: `shadowob:user:${senderId}`,
1273
1378
  To: `shadowob:channel:${channelId}`,
1274
1379
  SessionKey: route.sessionKey,
@@ -1285,6 +1390,8 @@ ${baseBodyForAgent}` : baseBodyForAgent;
1285
1390
  ...mentionContextFields(structuredMentions),
1286
1391
  OriginatingChannel: "shadowob",
1287
1392
  OriginatingTo: `shadowob:channel:${channelId}`,
1393
+ NativeChannelId: channelId,
1394
+ ...ownerAllowFrom ? { OwnerAllowFrom: ownerAllowFrom } : {},
1288
1395
  ...serverInfo ? {
1289
1396
  ServerId: serverInfo.serverId,
1290
1397
  ServerSlug: serverInfo.serverSlug,
@@ -1428,6 +1535,34 @@ ${baseBodyForAgent}` : baseBodyForAgent;
1428
1535
  }
1429
1536
  }
1430
1537
 
1538
+ // src/monitor/message-queue.ts
1539
+ function queueKeyForMessage(message) {
1540
+ return `${message.channelId}:${message.threadId ?? ""}`;
1541
+ }
1542
+ function createShadowMessageProcessingQueue(options) {
1543
+ const queues = /* @__PURE__ */ new Map();
1544
+ return {
1545
+ enqueue(message, source) {
1546
+ const key = queueKeyForMessage(message);
1547
+ const previous = queues.get(key) ?? Promise.resolve();
1548
+ const task = previous.catch(() => void 0).then(() => {
1549
+ if (options.isStopped?.()) {
1550
+ options.onSkipped?.(message, source);
1551
+ return;
1552
+ }
1553
+ return options.process(message, source);
1554
+ }).finally(() => {
1555
+ if (queues.get(key) === task) queues.delete(key);
1556
+ });
1557
+ queues.set(key, task);
1558
+ return task;
1559
+ },
1560
+ pendingKeys() {
1561
+ return [...queues.keys()];
1562
+ }
1563
+ };
1564
+ }
1565
+
1431
1566
  // src/runtime.ts
1432
1567
  import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
1433
1568
  var store = createPluginRuntimeStore(
@@ -1534,7 +1669,7 @@ async function monitorShadowProvider(options) {
1534
1669
  } else {
1535
1670
  runtime.log?.(`[config] Resolved agentId: ${agentId}`);
1536
1671
  }
1537
- const slashCommands = await loadLocalSlashCommands(runtime);
1672
+ const slashCommands = await loadShadowSlashCommands(runtime);
1538
1673
  if (agentId) {
1539
1674
  try {
1540
1675
  await runShadowApiOperation(
@@ -1554,6 +1689,23 @@ async function monitorShadowProvider(options) {
1554
1689
  const messageWatermarks = await loadMessageWatermarks(accountId);
1555
1690
  const processedMessageIds = /* @__PURE__ */ new Set();
1556
1691
  const catchupInFlight = /* @__PURE__ */ new Map();
1692
+ const buildAccessPolicyConfig = (config2) => {
1693
+ const activeTenantIds = config2?.activeTenantIds ?? [];
1694
+ const allowedTriggerUserIds = config2?.allowedTriggerUserIds ?? [config2?.ownerId, ...activeTenantIds].filter((id) => Boolean(id));
1695
+ return {
1696
+ allowedTriggerUserIds,
1697
+ triggerUserIds: allowedTriggerUserIds,
1698
+ ownerId: config2?.ownerId,
1699
+ activeTenantIds,
1700
+ replyRequiresMention: false
1701
+ };
1702
+ };
1703
+ const buildDefaultAccessPolicy = (config2) => ({
1704
+ listen: true,
1705
+ reply: true,
1706
+ mentionOnly: false,
1707
+ config: buildAccessPolicyConfig(config2)
1708
+ });
1557
1709
  const rememberProcessedMessage = (messageId) => {
1558
1710
  processedMessageIds.add(messageId);
1559
1711
  if (processedMessageIds.size > MAX_TRACKED_MESSAGE_IDS) {
@@ -1669,12 +1821,7 @@ async function monitorShadowProvider(options) {
1669
1821
  for (const ch of directChannels) {
1670
1822
  if (!allChannelIds.includes(ch.id)) allChannelIds.push(ch.id);
1671
1823
  if (!channelPolicies.has(ch.id)) {
1672
- channelPolicies.set(ch.id, {
1673
- listen: true,
1674
- reply: true,
1675
- mentionOnly: false,
1676
- config: {}
1677
- });
1824
+ channelPolicies.set(ch.id, buildDefaultAccessPolicy(remoteConfig));
1678
1825
  }
1679
1826
  }
1680
1827
  runtime.log?.(`[config] Monitoring ${directChannels.length} direct channel(s)`);
@@ -1735,6 +1882,13 @@ async function monitorShadowProvider(options) {
1735
1882
  );
1736
1883
  }
1737
1884
  };
1885
+ const messageQueue = createShadowMessageProcessingQueue({
1886
+ process: processChannelMessageWithRetry,
1887
+ isStopped: () => stopped,
1888
+ onSkipped: (message, source) => {
1889
+ runtime.log?.(`[${source}] Monitor stopped, skipping queued message ${message.id}`);
1890
+ }
1891
+ });
1738
1892
  const catchUpChannel = async (channelId, reason) => {
1739
1893
  try {
1740
1894
  const result = await client.getMessages(channelId, 50);
@@ -1759,7 +1913,7 @@ async function monitorShadowProvider(options) {
1759
1913
  }
1760
1914
  for (const message of candidates) {
1761
1915
  rememberProcessedMessage(message.id);
1762
- await processChannelMessageWithRetry(message, "catchup");
1916
+ await messageQueue.enqueue(message, "catchup");
1763
1917
  }
1764
1918
  } catch (err) {
1765
1919
  runtime.error?.(`[catchup] Failed for channel ${channelId}: ${String(err)}`);
@@ -1865,25 +2019,78 @@ async function monitorShadowProvider(options) {
1865
2019
  "agent:policy-changed",
1866
2020
  (data) => {
1867
2021
  if (data.agentId !== agentId) return;
1868
- if (!data.channelId) return;
1869
- const mentionOnly = data.mentionOnly ?? false;
2022
+ if (!data.channelId) {
2023
+ void (async () => {
2024
+ try {
2025
+ const updatedConfig = await runShadowApiOperation(
2026
+ "refresh remote config after access policy change",
2027
+ () => client.getAgentConfig(agentId),
2028
+ { runtime, abortSignal }
2029
+ );
2030
+ remoteConfig = updatedConfig;
2031
+ const remoteChannelIds = /* @__PURE__ */ new Set();
2032
+ const accessConfig2 = buildAccessPolicyConfig(updatedConfig);
2033
+ for (const server of updatedConfig.servers) {
2034
+ for (const ch of server.channels) {
2035
+ remoteChannelIds.add(ch.id);
2036
+ channelServerMap.set(ch.id, {
2037
+ serverId: server.id,
2038
+ serverSlug: server.slug ?? server.id,
2039
+ serverName: server.name,
2040
+ channelName: ch.name
2041
+ });
2042
+ channelPolicies.set(ch.id, {
2043
+ ...ch.policy,
2044
+ config: { ...ch.policy.config, ...accessConfig2 }
2045
+ });
2046
+ if (!allChannelIds.includes(ch.id)) {
2047
+ allChannelIds.push(ch.id);
2048
+ void socket.joinChannel(ch.id);
2049
+ }
2050
+ }
2051
+ }
2052
+ for (const [channelId] of channelServerMap) {
2053
+ if (remoteChannelIds.has(channelId)) continue;
2054
+ channelServerMap.delete(channelId);
2055
+ channelPolicies.delete(channelId);
2056
+ const idx = allChannelIds.indexOf(channelId);
2057
+ if (idx !== -1) allChannelIds.splice(idx, 1);
2058
+ socket.leaveChannel(channelId);
2059
+ }
2060
+ for (const [channelId, existing2] of channelPolicies) {
2061
+ if (channelServerMap.has(channelId)) continue;
2062
+ channelPolicies.set(channelId, {
2063
+ ...existing2,
2064
+ config: { ...existing2.config, ...accessConfig2 }
2065
+ });
2066
+ }
2067
+ runtime.log?.("[config] Refreshed Buddy owner/tenant access policy");
2068
+ } catch (err) {
2069
+ runtime.error?.(`[config] Failed to refresh access policy: ${String(err)}`);
2070
+ }
2071
+ })();
2072
+ return;
2073
+ }
2074
+ const existing = channelPolicies.get(data.channelId);
2075
+ const mentionOnly = data.mentionOnly ?? existing?.mentionOnly ?? false;
2076
+ const reply = data.reply ?? existing?.reply ?? true;
2077
+ const accessConfig = buildAccessPolicyConfig(remoteConfig);
1870
2078
  runtime.log?.(
1871
- `[ws] Received agent:policy-changed for channel ${data.channelId}: mentionOnly=${mentionOnly}, reply=${data.reply}, config=${JSON.stringify(data.config ?? {})}`
2079
+ `[ws] Received agent:policy-changed for channel ${data.channelId}: mentionOnly=${mentionOnly}, reply=${reply}, config=${JSON.stringify(data.config ?? {})}`
1872
2080
  );
1873
- const existing = channelPolicies.get(data.channelId);
1874
2081
  if (existing) {
1875
2082
  channelPolicies.set(data.channelId, {
1876
2083
  ...existing,
1877
2084
  mentionOnly,
1878
- reply: data.reply ?? existing.reply,
1879
- config: data.config ?? existing.config
2085
+ reply,
2086
+ config: { ...existing.config, ...accessConfig, ...data.config ?? {} }
1880
2087
  });
1881
2088
  } else {
1882
2089
  channelPolicies.set(data.channelId, {
1883
2090
  listen: true,
1884
- reply: data.reply ?? true,
2091
+ reply,
1885
2092
  mentionOnly,
1886
- config: data.config ?? {}
2093
+ config: { ...accessConfig, ...data.config ?? {} }
1887
2094
  });
1888
2095
  }
1889
2096
  }
@@ -1934,12 +2141,12 @@ async function monitorShadowProvider(options) {
1934
2141
  if (!refreshed) {
1935
2142
  await resolveChannelContext(data.channelId, "member-added");
1936
2143
  }
1937
- if (!refreshed && !channelPolicies.has(data.channelId)) {
2144
+ if (!channelPolicies.has(data.channelId)) {
1938
2145
  const defaultPolicy = {
1939
2146
  listen: true,
1940
2147
  reply: true,
1941
2148
  mentionOnly: false,
1942
- config: {}
2149
+ config: buildAccessPolicyConfig(remoteConfig)
1943
2150
  };
1944
2151
  channelPolicies.set(data.channelId, defaultPolicy);
1945
2152
  }
@@ -1983,7 +2190,7 @@ async function monitorShadowProvider(options) {
1983
2190
  runtime.log?.(`[ws] Message from unmonitored channel ${message.channelId}, ignoring`);
1984
2191
  return;
1985
2192
  }
1986
- void processChannelMessageWithRetry(message, "ws");
2193
+ void messageQueue.enqueue(message, "ws");
1987
2194
  });
1988
2195
  socket.connect();
1989
2196
  const stop = () => {
package/dist/index.js CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  resolveOutboundMentions,
16
16
  setShadowRuntime,
17
17
  tryGetShadowRuntime
18
- } from "./chunk-PEV3R2R7.js";
18
+ } from "./chunk-4G572ZLC.js";
19
19
 
20
20
  // index.ts
21
21
  import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
@@ -808,6 +808,7 @@ var shadowAgentPromptHints = [
808
808
  "- When a Shadow user asks for buttons, choices, a select menu, a form, or approval, prefer sending a Shadow interactive dialog instead of plain text options.",
809
809
  "- ShadowOwnBuddy enables inline buttons, forms, and file uploads for Shadow channels by default. Do not tell the user that `shadowob.capabilities.inlineButtons` or file sending is unavailable; use the message tool instead.",
810
810
  '- Shadow interactive dialogs use the shared message tool with `action: "send"` plus `target`, `message`, `kind`, `prompt`, and shape fields. `message` is required by the shared tool; set `message` and `prompt` to the same user-visible text unless there is a specific reason not to. Supported `kind` values are `buttons`, `select`, `form`, and `approval`; Shadow stores these as `metadata.interactive` so the user can answer in-channel.',
811
+ "- When your next action is a Shadow interactive dialog, put the full user-visible prompt in that dialog and do not also send a separate ordinary reply in the same turn. This prevents duplicate form-plus-commentary messages.",
811
812
  '- Example buttons dialog: `action: "send"`, `target: "shadowob:channel:<ChannelId>"`, `message: "Choose the next step"`, `kind: "buttons"`, `prompt: "Choose the next step"`, `buttons: [{"id":"icp","label":"ICP / JTBD","value":"icp"}]`.',
812
813
  '- Example form dialog: `action: "send"`, `target: "shadowob:channel:<ChannelId>"`, `message: "Fill the decision inputs"`, `kind: "form"`, `fields: [{"id":"decision","label":"Decision","kind":"textarea","required":true}]`.',
813
814
  '- When Shadow context includes CommerceOfferIds and the user is interested in buying, viewing pricing, or receiving a product card, call the shared message tool yourself: `action: "send"`, `target: "shadowob:channel:<ChannelId>"`, `message: "A natural sales message"`, `commerceOfferId: "<CommerceOfferId>"`. Plain final text will not attach a product card.',
@@ -959,7 +960,7 @@ shadowPlugin.gateway = {
959
960
  lastError: null
960
961
  });
961
962
  ctx.log?.info(`Starting Shadow connection for account ${accountId}`);
962
- const { monitorShadowProvider: monitorShadowProvider2 } = await import("./monitor-AE3LRQYD.js");
963
+ const { monitorShadowProvider: monitorShadowProvider2 } = await import("./monitor-XDUBTELB.js");
963
964
  await monitorShadowProvider2({
964
965
  account,
965
966
  accountId,
@@ -5,7 +5,7 @@ import {
5
5
  normalizeShadowSlashCommands,
6
6
  resolveShadowAgentIdFromConfig,
7
7
  shouldCatchUpShadowMessage
8
- } from "./chunk-PEV3R2R7.js";
8
+ } from "./chunk-4G572ZLC.js";
9
9
  export {
10
10
  formatSlashCommandPrompt,
11
11
  matchShadowSlashCommand,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shadowob/openclaw-shadowob",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
4
4
  "description": "OpenClaw Shadow channel plugin — enables AI agents to interact in Shadow server channels",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -58,7 +58,7 @@
58
58
  "dependencies": {
59
59
  "zod": "^3.25.67",
60
60
  "openclaw": "^2026.5.7",
61
- "@shadowob/sdk": "1.1.3"
61
+ "@shadowob/sdk": "1.1.5"
62
62
  },
63
63
  "devDependencies": {
64
64
  "@types/node": "^22.15.21",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: shadowob
3
- description: "Shadow server CLI complete command-line interface for Shadow servers including servers, channels, DMs, workspace, shop, apps, agents, marketplace, OAuth, and more"
3
+ description: "Use when live Shadow context or actions are needed: channel/DM history, pins, members, server/channel/workspace/shop/app/agent data, or sending/managing Shadow content via the shadowob CLI."
4
4
  metadata:
5
5
  {
6
6
  "openclaw":
@@ -17,6 +17,10 @@ allowed-tools: ["exec"]
17
17
 
18
18
  Use `shadowob` CLI to interact with Shadow servers.
19
19
 
20
+ Activate this skill when you need current Shadow context, such as recent channel or DM history,
21
+ pinned messages, member/server/channel state, workspace/shop/app/agent data, or when you need to
22
+ send or manage Shadow content. Prefer narrow `--json` reads before acting.
23
+
20
24
  ## Quickstart
21
25
 
22
26
  ```bash
@@ -66,11 +70,6 @@ shadowob servers leave <server-id>
66
70
  # Members
67
71
  shadowob servers members <server-id> --json
68
72
 
69
- # Homepage
70
- shadowob servers homepage <server-id>
71
- shadowob servers homepage <server-id> --set <file.html>
72
- shadowob servers homepage <server-id> --clear
73
-
74
73
  # Discover public servers
75
74
  shadowob servers discover --json
76
75
  ```
@@ -179,28 +178,49 @@ shadowob workspace stats <server-id> --json
179
178
  shadowob workspace children <server-id> [--parent-id <id>] --json
180
179
 
181
180
  # Files
182
- shadowob workspace files get <file-id> --json
183
- shadowob workspace files create <server-id> --name <name> [--content <text>] [--parent-id <id>] --json
184
- shadowob workspace files update <file-id> [--name <name>] [--content <text>] --json
185
- shadowob workspace files delete <file-id>
181
+ shadowob workspace files get <server-id> <file-id> --json
186
182
  shadowob workspace files upload <server-id> --file <path> [--name <name>] [--parent-id <id>] --json
187
- shadowob workspace files download <file-id> [--output <path>]
183
+ shadowob workspace files update <server-id> <file-id> [--name <name>] [--parent-id <id>] --json
184
+ shadowob workspace files delete <server-id> <file-id>
185
+ shadowob workspace files search <server-id> [--search-text <text>] [--ext <ext>] [--parent-id <id>] --json
186
+ # Note: files download is not yet implemented in CLI; download via contentRef URL instead.
188
187
 
189
188
  # Folders
190
189
  shadowob workspace folders create <server-id> --name <name> [--parent-id <id>] --json
191
- shadowob workspace folders update <folder-id> --name <name> --json
192
- shadowob workspace folders delete <folder-id>
190
+ shadowob workspace folders update <server-id> <folder-id> [--name <name>] [--parent-id <id>] --json
191
+ shadowob workspace folders delete <server-id> <folder-id>
193
192
  ```
194
193
 
194
+ ### Workspace Node Metadata
195
+
196
+ Each workspace node has a `flags` JSONB field with optional metadata:
197
+
198
+ - **Access control**: `flags.access = { scope: "server" | "channel", serverId, channelId? }`. All nodes have at least `scope: "server"` + `serverId`. Channel-scoped nodes require channel membership for access.
199
+ - **Traceability**: `flags.source = "channel_message_attachment"` with `channelId` and `messageId` for files uploaded via channel messages, enabling reverse lookup to the originating message.
200
+ - **Path is server-computed**: `path` is derived from parent path + name, maintained server-side. Do not set path manually — it is auto-updated on rename/move.
201
+
195
202
  ## Shop
196
203
 
197
204
  ```bash
198
205
  # Shop info
199
206
  shadowob shop get <server-id> --json
207
+ shadowob shop get-by-id <shop-id> --json
208
+ shadowob shop me get --json
200
209
 
201
210
  # Products
202
- shadowob shop products list <server-id> --json
211
+ shadowob shop products list <server-id> [--status active] [--keyword <text>] [--limit <n>] --json
212
+ shadowob shop products list-by-shop <shop-id> [--status active] [--limit <n>] --json
203
213
  shadowob shop products get <server-id> <product-id> --json
214
+ shadowob shop products context <product-id> --json
215
+ shadowob shop products purchase <shop-id> <product-id> --idempotency-key <unique-operation-id> --json
216
+
217
+ # Offers, deliverables, and shop assets
218
+ shadowob shop offers list <shop-id> --json
219
+ shadowob shop offers create <shop-id> --data '<offer-json>' --json
220
+ shadowob shop offers deliverables create <shop-id> <offer-id> --data '<deliverable-json>' --json
221
+ shadowob shop assets list <shop-id> --json
222
+ shadowob shop assets create <shop-id> --data '<asset-definition-json>' --json
223
+ shadowob shop entitlements list <shop-id> --json
204
224
 
205
225
  # Cart
206
226
  shadowob shop cart list <server-id> --json
@@ -213,10 +233,64 @@ shadowob shop orders get <server-id> <order-id> --json
213
233
  shadowob shop wallet balance --json
214
234
  ```
215
235
 
236
+ ## Commerce
237
+
238
+ ```bash
239
+ # Product and offer buyer context
240
+ shadowob commerce products context <product-id> --json
241
+ shadowob commerce offers preview <offer-id> --json
242
+ shadowob commerce offers purchase <offer-id> --idempotency-key <unique-operation-id> --json
243
+
244
+ # Chat commerce cards
245
+ shadowob commerce cards list --channel-id <channel-id> [--keyword <text>] --json
246
+ shadowob commerce cards purchase <message-id> <card-id> --idempotency-key <unique-operation-id> --json
247
+
248
+ # Purchases, delivery, protected files, and community assets
249
+ shadowob commerce entitlements list [--server-id <server-id>] --json
250
+ shadowob commerce entitlements get <entitlement-id> --json
251
+ shadowob commerce entitlements verify <entitlement-id> --json
252
+ shadowob commerce paid-files open <file-id> --json
253
+ shadowob commerce assets list --json
254
+ shadowob commerce assets consume <grant-id> --idempotency-key <unique-operation-id> --json
255
+
256
+ # Seller income and support actions
257
+ shadowob commerce settlements list --json
258
+ shadowob commerce settlements settle --json
259
+ shadowob commerce tips send --recipient-user-id <user-id> --amount <shrimp> [--message <text>] --json
260
+ shadowob commerce gifts send --recipient-user-id <user-id> --assets '<json-array>' --json
261
+ ```
262
+
263
+ ## Commerce Validation Notes
264
+
265
+ - Use the CLI for setup, inspection, and automation, but validate commerce user stories in the
266
+ browser before calling them complete.
267
+ - Do not add seed code to populate commerce surfaces. Create ordinary local/test records through
268
+ browser flows or explicit setup calls.
269
+ - When inspecting a commerce flow, preserve ids for the handoff: product, offer, order,
270
+ entitlement, shop, server, Buddy, and workspace file where applicable.
271
+ - External app entitlement automation must use Shadow OAuth commerce APIs and remain scoped to the
272
+ app's own `external_app` resource namespace.
273
+
216
274
  ## Apps
217
275
 
218
276
  ```bash
219
- # List apps
277
+ # Server App integrations
278
+ shadowob app list --server <server-id-or-slug> --json
279
+ shadowob app preview --server <server-id-or-slug> --manifest-url <manifest-url> --json
280
+ shadowob app discover --server <server-id-or-slug> --json
281
+ shadowob app inspect <app-key> --server <server-id-or-slug> --json
282
+ shadowob app skills <app-key> --server <server-id-or-slug>
283
+ shadowob app call <app-key> <command> --server <server-id-or-slug> --json-input '<raw-command-input-json>' --json
284
+ ```
285
+
286
+ For server App commands, use the `shadowob app` CLI path only. Do not use curl, fetch, raw HTTP
287
+ routes, or the JavaScript SDK to call server App commands. Pass the command input object directly
288
+ to `--json-input`, for example `{"title":"Example","priority":"high"}`; the CLI wraps the HTTP
289
+ request for you and binds Shadow OAuth identity, server membership, App grants, and command policy.
290
+ When a channel message mentions a server App, use the mentioned app key/server id directly.
291
+
292
+ ```bash
293
+ # Legacy workspace apps
220
294
  shadowob apps list <server-id> --json
221
295
 
222
296
  # Get app
@@ -302,6 +376,10 @@ shadowob oauth consents --json
302
376
 
303
377
  # Revoke consent for an app
304
378
  shadowob oauth revoke <app-id>
379
+
380
+ # External app commerce entitlement checks use OAuth access tokens, not user JWTs
381
+ shadowob oauth commerce check --access-token <oauth-access-token> --resource-id <app-id>:premium --json
382
+ shadowob oauth commerce redeem --access-token <oauth-access-token> --resource-id <app-id>:premium --idempotency-key <provider-operation-id> --json
305
383
  ```
306
384
 
307
385
  ## Marketplace