feat(commands): unify chat commands (#275)

* Chat commands: registry, access groups, Carbon

* Chat commands: clear native commands on disable

* fix(commands): align command surface typing

* docs(changelog): note commands registry (PR #275)

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
Shadow
2026-01-06 14:17:56 -06:00
committed by GitHub
parent 1bf44bf30c
commit 9b22e1f6e9
40 changed files with 2357 additions and 1459 deletions

View File

@@ -8,6 +8,11 @@ import {
resolveTextChunkLimit,
} from "../auto-reply/chunk.js";
import { hasControlCommand } from "../auto-reply/command-detection.js";
import {
buildCommandText,
listNativeCommandSpecs,
shouldHandleTextCommands,
} from "../auto-reply/commands-registry.js";
import { formatAgentEnvelope } from "../auto-reply/envelope.js";
import { dispatchReplyFromConfig } from "../auto-reply/reply/dispatch-from-config.js";
import {
@@ -389,6 +394,7 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
const channelsConfig = cfg.slack?.channels;
const dmEnabled = dmConfig?.enabled ?? true;
const groupPolicy = cfg.slack?.groupPolicy ?? "open";
const useAccessGroups = cfg.commands?.useAccessGroups !== false;
const reactionMode = cfg.slack?.reactionNotifications ?? "own";
const reactionAllowlist = cfg.slack?.reactionAllowlist ?? [];
const slashCommand = resolveSlackSlashCommandConfig(
@@ -672,7 +678,12 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
name: senderName,
});
const hasAnyMention = /<@[^>]+>/.test(message.text ?? "");
const allowTextCommands = shouldHandleTextCommands({
cfg,
surface: "slack",
});
const shouldBypassMention =
allowTextCommands &&
isRoom &&
channelConfig?.requireMention &&
!wasMentioned &&
@@ -1301,193 +1312,242 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
},
);
if (slashCommand.enabled) {
const handleSlashCommand = async (params: {
command: SlackCommandMiddlewareArgs["command"];
ack: SlackCommandMiddlewareArgs["ack"];
respond: SlackCommandMiddlewareArgs["respond"];
prompt: string;
}) => {
const { command, ack, respond, prompt } = params;
try {
if (!prompt.trim()) {
await ack({
text: "Message required.",
response_type: "ephemeral",
});
return;
}
await ack();
if (botUserId && command.user_id === botUserId) return;
const channelInfo = await resolveChannelName(command.channel_id);
const channelType =
channelInfo?.type ??
(command.channel_name === "directmessage" ? "im" : undefined);
const isDirectMessage = channelType === "im";
const isGroupDm = channelType === "mpim";
const isRoom = channelType === "channel" || channelType === "group";
if (isDirectMessage && !dmEnabled) {
await respond({
text: "Slack DMs are disabled.",
response_type: "ephemeral",
});
return;
}
if (isGroupDm && !groupDmEnabled) {
await respond({
text: "Slack group DMs are disabled.",
response_type: "ephemeral",
});
return;
}
if (isGroupDm && groupDmChannels.length > 0) {
const allowList = normalizeAllowListLower(groupDmChannels);
const channelName = channelInfo?.name;
const candidates = [
command.channel_id,
channelName ? `#${channelName}` : undefined,
channelName,
channelName ? normalizeSlackSlug(channelName) : undefined,
]
.filter((value): value is string => Boolean(value))
.map((value) => value.toLowerCase());
const permitted =
allowList.includes("*") ||
candidates.some((candidate) => allowList.includes(candidate));
if (!permitted) {
await respond({
text: "This group DM is not allowed.",
response_type: "ephemeral",
});
return;
}
}
const storeAllowFrom = await readProviderAllowFromStore("slack").catch(
() => [],
);
const effectiveAllowFrom = normalizeAllowList([
...allowFrom,
...storeAllowFrom,
]);
const effectiveAllowFromLower =
normalizeAllowListLower(effectiveAllowFrom);
let commandAuthorized = true;
if (isDirectMessage) {
if (!dmEnabled || dmPolicy === "disabled") {
await respond({
text: "Slack DMs are disabled.",
response_type: "ephemeral",
});
return;
}
if (dmPolicy !== "open") {
const sender = await resolveUserName(command.user_id);
const senderName = sender?.name ?? undefined;
const permitted = allowListMatches({
allowList: effectiveAllowFromLower,
id: command.user_id,
name: senderName,
});
if (!permitted) {
if (dmPolicy === "pairing") {
const { code } = await upsertProviderPairingRequest({
provider: "slack",
id: command.user_id,
meta: { name: senderName },
});
await respond({
text: [
"Clawdbot: access not configured.",
"",
`Pairing code: ${code}`,
"",
"Ask the bot owner to approve with:",
"clawdbot pairing approve --provider slack <code>",
].join("\n"),
response_type: "ephemeral",
});
} else {
await respond({
text: "You are not authorized to use this command.",
response_type: "ephemeral",
});
}
return;
}
commandAuthorized = true;
}
}
if (isRoom) {
const channelConfig = resolveSlackChannelConfig({
channelId: command.channel_id,
channelName: channelInfo?.name,
channels: channelsConfig,
});
if (
useAccessGroups &&
!isSlackRoomAllowedByPolicy({
groupPolicy,
channelAllowlistConfigured:
Boolean(channelsConfig) &&
Object.keys(channelsConfig ?? {}).length > 0,
channelAllowed: channelConfig?.allowed !== false,
})
) {
await respond({
text: "This channel is not allowed.",
response_type: "ephemeral",
});
return;
}
if (useAccessGroups && channelConfig?.allowed === false) {
await respond({
text: "This channel is not allowed.",
response_type: "ephemeral",
});
return;
}
}
const sender = await resolveUserName(command.user_id);
const senderName = sender?.name ?? command.user_name ?? command.user_id;
const channelName = channelInfo?.name;
const roomLabel = channelName
? `#${channelName}`
: `#${command.channel_id}`;
const isRoomish = isRoom || isGroupDm;
const route = resolveAgentRoute({
cfg,
provider: "slack",
teamId: teamId || undefined,
peer: {
kind: isDirectMessage ? "dm" : isRoom ? "channel" : "group",
id: isDirectMessage ? command.user_id : command.channel_id,
},
});
const ctxPayload = {
Body: prompt,
From: isDirectMessage
? `slack:${command.user_id}`
: isRoom
? `slack:channel:${command.channel_id}`
: `slack:group:${command.channel_id}`,
To: `slash:${command.user_id}`,
ChatType: isDirectMessage ? "direct" : isRoom ? "room" : "group",
GroupSubject: isRoomish ? roomLabel : undefined,
SenderName: senderName,
SenderId: command.user_id,
Provider: "slack" as const,
Surface: "slack" as const,
WasMentioned: true,
MessageSid: command.trigger_id,
Timestamp: Date.now(),
SessionKey: `agent:${route.agentId}:${slashCommand.sessionPrefix}:${command.user_id}`,
AccountId: route.accountId,
CommandSource: "native" as const,
CommandAuthorized: commandAuthorized,
};
const replyResult = await getReplyFromConfig(ctxPayload, undefined, cfg);
const replies = replyResult
? Array.isArray(replyResult)
? replyResult
: [replyResult]
: [];
await deliverSlackSlashReplies({
replies,
respond,
ephemeral: slashCommand.ephemeral,
textLimit,
});
} catch (err) {
runtime.error?.(danger(`slack slash handler failed: ${String(err)}`));
await respond({
text: "Sorry, something went wrong handling that command.",
response_type: "ephemeral",
});
}
};
const nativeCommands =
cfg.commands?.native === true ? listNativeCommandSpecs() : [];
if (nativeCommands.length > 0) {
for (const command of nativeCommands) {
app.command(
`/${command.name}`,
async ({ command: cmd, ack, respond }: SlackCommandMiddlewareArgs) => {
const prompt = buildCommandText(command.name, cmd.text);
await handleSlashCommand({ command: cmd, ack, respond, prompt });
},
);
}
} else if (slashCommand.enabled) {
app.command(
slashCommand.name,
async ({ command, ack, respond }: SlackCommandMiddlewareArgs) => {
try {
const prompt = command.text?.trim();
if (!prompt) {
await ack({
text: "Message required.",
response_type: "ephemeral",
});
return;
}
await ack();
if (botUserId && command.user_id === botUserId) return;
const channelInfo = await resolveChannelName(command.channel_id);
const channelType =
channelInfo?.type ??
(command.channel_name === "directmessage" ? "im" : undefined);
const isDirectMessage = channelType === "im";
const isGroupDm = channelType === "mpim";
const isRoom = channelType === "channel" || channelType === "group";
if (isDirectMessage && !dmEnabled) {
await respond({
text: "Slack DMs are disabled.",
response_type: "ephemeral",
});
return;
}
if (isGroupDm && !groupDmEnabled) {
await respond({
text: "Slack group DMs are disabled.",
response_type: "ephemeral",
});
return;
}
if (isGroupDm && groupDmChannels.length > 0) {
const allowList = normalizeAllowListLower(groupDmChannels);
const channelName = channelInfo?.name;
const candidates = [
command.channel_id,
channelName ? `#${channelName}` : undefined,
channelName,
channelName ? normalizeSlackSlug(channelName) : undefined,
]
.filter((value): value is string => Boolean(value))
.map((value) => value.toLowerCase());
const permitted =
allowList.includes("*") ||
candidates.some((candidate) => allowList.includes(candidate));
if (!permitted) {
await respond({
text: "This group DM is not allowed.",
response_type: "ephemeral",
});
return;
}
}
if (isDirectMessage) {
if (!dmEnabled || dmPolicy === "disabled") {
await respond({
text: "Slack DMs are disabled.",
response_type: "ephemeral",
});
return;
}
if (dmPolicy !== "open") {
const storeAllowFrom = await readProviderAllowFromStore(
"slack",
).catch(() => []);
const effectiveAllowFrom = normalizeAllowList([
...allowFrom,
...storeAllowFrom,
]);
const sender = await resolveUserName(command.user_id);
const permitted = allowListMatches({
allowList: normalizeAllowListLower(effectiveAllowFrom),
id: command.user_id,
name: sender?.name ?? undefined,
});
if (!permitted) {
if (dmPolicy === "pairing") {
const senderName = sender?.name ?? undefined;
const { code } = await upsertProviderPairingRequest({
provider: "slack",
id: command.user_id,
meta: { name: senderName },
});
await respond({
text: [
"Clawdbot: access not configured.",
"",
`Pairing code: ${code}`,
"",
"Ask the bot owner to approve with:",
"clawdbot pairing approve --provider slack <code>",
].join("\n"),
response_type: "ephemeral",
});
} else {
await respond({
text: "You are not authorized to use this command.",
response_type: "ephemeral",
});
}
return;
}
}
}
if (isRoom) {
const channelConfig = resolveSlackChannelConfig({
channelId: command.channel_id,
channelName: channelInfo?.name,
channels: channelsConfig,
});
if (channelConfig?.allowed === false) {
await respond({
text: "This channel is not allowed.",
response_type: "ephemeral",
});
return;
}
}
const sender = await resolveUserName(command.user_id);
const senderName =
sender?.name ?? command.user_name ?? command.user_id;
const channelName = channelInfo?.name;
const roomLabel = channelName
? `#${channelName}`
: `#${command.channel_id}`;
const isRoomish = isRoom || isGroupDm;
const route = resolveAgentRoute({
cfg,
provider: "slack",
teamId: teamId || undefined,
peer: { kind: "dm", id: command.user_id },
});
const ctxPayload = {
Body: prompt,
From: isDirectMessage
? `slack:${command.user_id}`
: isRoom
? `slack:channel:${command.channel_id}`
: `slack:group:${command.channel_id}`,
To: `slash:${command.user_id}`,
ChatType: isDirectMessage ? "direct" : isRoom ? "room" : "group",
GroupSubject: isRoomish ? roomLabel : undefined,
SenderName: senderName,
Provider: "slack" as const,
WasMentioned: true,
MessageSid: command.trigger_id,
Timestamp: Date.now(),
SessionKey: `agent:${route.agentId}:${slashCommand.sessionPrefix}:${command.user_id}`,
AccountId: route.accountId,
};
const replyResult = await getReplyFromConfig(
ctxPayload,
undefined,
cfg,
);
const replies = replyResult
? Array.isArray(replyResult)
? replyResult
: [replyResult]
: [];
await deliverSlackSlashReplies({
replies,
respond,
ephemeral: slashCommand.ephemeral,
textLimit,
});
} catch (err) {
runtime.error?.(danger(`slack slash handler failed: ${String(err)}`));
await respond({
text: "Sorry, something went wrong handling that command.",
response_type: "ephemeral",
});
}
await handleSlashCommand({
command,
ack,
respond,
prompt: command.text?.trim() ?? "",
});
},
);
}