mirror of https://github.com/openclaw/openclaw.git
Slack: redact and cap interaction system events (#28982)
This commit is contained in:
parent
e0571399ac
commit
746688ddc9
|
|
@ -214,8 +214,8 @@ describe("registerSlackInteractionEvents", () => {
|
|||
value: "approved",
|
||||
userId: "U123",
|
||||
teamId: "T9",
|
||||
triggerId: "123.trigger",
|
||||
responseUrl: "https://hooks.slack.test/response",
|
||||
triggerId: "[redacted]",
|
||||
responseUrl: "[redacted]",
|
||||
channelId: "C1",
|
||||
messageTs: "100.200",
|
||||
threadTs: "100.100",
|
||||
|
|
@ -885,7 +885,7 @@ describe("registerSlackInteractionEvents", () => {
|
|||
};
|
||||
expect(payload).toMatchObject({
|
||||
actionType: "workflow_button",
|
||||
workflowTriggerUrl: "https://slack.com/workflows/triggers/T420/12345",
|
||||
workflowTriggerUrl: "[redacted]",
|
||||
workflowId: "Wf12345",
|
||||
teamId: "T420",
|
||||
channelId: "C420",
|
||||
|
|
@ -979,7 +979,7 @@ describe("registerSlackInteractionEvents", () => {
|
|||
rootViewId: "VROOT",
|
||||
previousViewId: "VPREV",
|
||||
externalId: "deploy-ext-1",
|
||||
viewHash: "view-hash-1",
|
||||
viewHash: "[redacted]",
|
||||
isStackedView: true,
|
||||
});
|
||||
expect(payload.inputs).toEqual(
|
||||
|
|
@ -1378,14 +1378,11 @@ describe("registerSlackInteractionEvents", () => {
|
|||
viewId: "V900",
|
||||
userId: "U900",
|
||||
isCleared: true,
|
||||
privateMetadata: JSON.stringify({
|
||||
sessionKey: "agent:main:slack:channel:C99",
|
||||
userId: "U900",
|
||||
}),
|
||||
privateMetadata: "[redacted]",
|
||||
rootViewId: "VROOT900",
|
||||
previousViewId: "VPREV900",
|
||||
externalId: "deploy-ext-900",
|
||||
viewHash: "view-hash-900",
|
||||
viewHash: "[redacted]",
|
||||
isStackedView: true,
|
||||
});
|
||||
expect(payload.inputs).toEqual(
|
||||
|
|
@ -1426,5 +1423,64 @@ describe("registerSlackInteractionEvents", () => {
|
|||
expect(payload.interactionType).toBe("view_closed");
|
||||
expect(payload.isCleared).toBe(false);
|
||||
});
|
||||
|
||||
it("caps oversized interaction payloads with compact summaries", async () => {
|
||||
enqueueSystemEventMock.mockClear();
|
||||
const { ctx, getViewHandler } = createContext();
|
||||
registerSlackInteractionEvents({ ctx: ctx as never });
|
||||
const viewHandler = getViewHandler();
|
||||
expect(viewHandler).toBeTruthy();
|
||||
|
||||
const richTextValue = {
|
||||
type: "rich_text",
|
||||
elements: Array.from({ length: 20 }, (_, index) => ({
|
||||
type: "rich_text_section",
|
||||
elements: [{ type: "text", text: `chunk-${index}-${"x".repeat(400)}` }],
|
||||
})),
|
||||
};
|
||||
const values: Record<string, Record<string, unknown>> = {};
|
||||
for (let index = 0; index < 20; index += 1) {
|
||||
values[`block_${index}`] = {
|
||||
[`input_${index}`]: {
|
||||
type: "rich_text_input",
|
||||
rich_text_value: richTextValue,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const ack = vi.fn().mockResolvedValue(undefined);
|
||||
await viewHandler!({
|
||||
ack,
|
||||
body: {
|
||||
user: { id: "U915" },
|
||||
team: { id: "T1" },
|
||||
view: {
|
||||
id: "V915",
|
||||
callback_id: "openclaw:oversize",
|
||||
private_metadata: JSON.stringify({
|
||||
channelId: "D915",
|
||||
channelType: "im",
|
||||
userId: "U915",
|
||||
}),
|
||||
state: {
|
||||
values,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as never);
|
||||
|
||||
expect(ack).toHaveBeenCalled();
|
||||
expect(enqueueSystemEventMock).toHaveBeenCalledTimes(1);
|
||||
const [eventText] = enqueueSystemEventMock.mock.calls[0] as [string];
|
||||
expect(eventText.length).toBeLessThanOrEqual(2400);
|
||||
const payload = JSON.parse(eventText.replace("Slack interaction: ", "")) as {
|
||||
payloadTruncated?: boolean;
|
||||
inputs?: unknown[];
|
||||
inputsOmitted?: number;
|
||||
};
|
||||
expect(payload.payloadTruncated).toBe(true);
|
||||
expect(Array.isArray(payload.inputs) ? payload.inputs.length : 0).toBeLessThanOrEqual(3);
|
||||
expect((payload.inputsOmitted ?? 0) >= 1).toBe(true);
|
||||
});
|
||||
});
|
||||
const selectedDateTimeEpoch = 1_771_632_300;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,19 @@ import { escapeSlackMrkdwn } from "../mrkdwn.js";
|
|||
|
||||
// Prefix for OpenClaw-generated action IDs to scope our handler
|
||||
const OPENCLAW_ACTION_PREFIX = "openclaw:";
|
||||
const SLACK_INTERACTION_EVENT_PREFIX = "Slack interaction: ";
|
||||
const REDACTED_INTERACTION_VALUE = "[redacted]";
|
||||
const SLACK_INTERACTION_EVENT_MAX_CHARS = 2400;
|
||||
const SLACK_INTERACTION_STRING_MAX_CHARS = 160;
|
||||
const SLACK_INTERACTION_ARRAY_MAX_ITEMS = 64;
|
||||
const SLACK_INTERACTION_COMPACT_INPUTS_MAX_ITEMS = 3;
|
||||
const SLACK_INTERACTION_REDACTED_KEYS = new Set([
|
||||
"triggerId",
|
||||
"responseUrl",
|
||||
"workflowTriggerUrl",
|
||||
"privateMetadata",
|
||||
"viewHash",
|
||||
]);
|
||||
|
||||
type InteractionMessageBlock = {
|
||||
type?: string;
|
||||
|
|
@ -107,6 +120,145 @@ type RegisterSlackModalHandler = (
|
|||
handler: (args: SlackModalEventHandlerArgs) => Promise<void>,
|
||||
) => void;
|
||||
|
||||
function truncateInteractionString(
|
||||
value: string,
|
||||
max = SLACK_INTERACTION_STRING_MAX_CHARS,
|
||||
): string {
|
||||
const trimmed = value.trim();
|
||||
if (trimmed.length <= max) {
|
||||
return trimmed;
|
||||
}
|
||||
return `${trimmed.slice(0, max - 1)}…`;
|
||||
}
|
||||
|
||||
function sanitizeSlackInteractionPayloadValue(value: unknown, key?: string): unknown {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (key && SLACK_INTERACTION_REDACTED_KEYS.has(key)) {
|
||||
if (typeof value !== "string" || value.trim().length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return REDACTED_INTERACTION_VALUE;
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
return truncateInteractionString(value);
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
const sanitized = value
|
||||
.slice(0, SLACK_INTERACTION_ARRAY_MAX_ITEMS)
|
||||
.map((entry) => sanitizeSlackInteractionPayloadValue(entry))
|
||||
.filter((entry) => entry !== undefined);
|
||||
if (value.length > SLACK_INTERACTION_ARRAY_MAX_ITEMS) {
|
||||
sanitized.push(`…+${value.length - SLACK_INTERACTION_ARRAY_MAX_ITEMS} more`);
|
||||
}
|
||||
return sanitized;
|
||||
}
|
||||
if (!value || typeof value !== "object") {
|
||||
return value;
|
||||
}
|
||||
const output: Record<string, unknown> = {};
|
||||
for (const [entryKey, entryValue] of Object.entries(value as Record<string, unknown>)) {
|
||||
const sanitized = sanitizeSlackInteractionPayloadValue(entryValue, entryKey);
|
||||
if (sanitized === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (typeof sanitized === "string" && sanitized.length === 0) {
|
||||
continue;
|
||||
}
|
||||
if (Array.isArray(sanitized) && sanitized.length === 0) {
|
||||
continue;
|
||||
}
|
||||
output[entryKey] = sanitized;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function buildCompactSlackInteractionPayload(
|
||||
payload: Record<string, unknown>,
|
||||
): Record<string, unknown> {
|
||||
const rawInputs = Array.isArray(payload.inputs) ? payload.inputs : [];
|
||||
const compactInputs = rawInputs
|
||||
.slice(0, SLACK_INTERACTION_COMPACT_INPUTS_MAX_ITEMS)
|
||||
.flatMap((entry) => {
|
||||
if (!entry || typeof entry !== "object") {
|
||||
return [];
|
||||
}
|
||||
const typed = entry as Record<string, unknown>;
|
||||
return [
|
||||
{
|
||||
actionId: typed.actionId,
|
||||
blockId: typed.blockId,
|
||||
actionType: typed.actionType,
|
||||
inputKind: typed.inputKind,
|
||||
selectedValues: typed.selectedValues,
|
||||
selectedLabels: typed.selectedLabels,
|
||||
inputValue: typed.inputValue,
|
||||
inputNumber: typed.inputNumber,
|
||||
selectedDate: typed.selectedDate,
|
||||
selectedTime: typed.selectedTime,
|
||||
selectedDateTime: typed.selectedDateTime,
|
||||
richTextPreview: typed.richTextPreview,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
return {
|
||||
interactionType: payload.interactionType,
|
||||
actionId: payload.actionId,
|
||||
callbackId: payload.callbackId,
|
||||
actionType: payload.actionType,
|
||||
userId: payload.userId,
|
||||
teamId: payload.teamId,
|
||||
channelId: payload.channelId ?? payload.routedChannelId,
|
||||
messageTs: payload.messageTs,
|
||||
threadTs: payload.threadTs,
|
||||
viewId: payload.viewId,
|
||||
isCleared: payload.isCleared,
|
||||
selectedValues: payload.selectedValues,
|
||||
selectedLabels: payload.selectedLabels,
|
||||
selectedDate: payload.selectedDate,
|
||||
selectedTime: payload.selectedTime,
|
||||
selectedDateTime: payload.selectedDateTime,
|
||||
workflowId: payload.workflowId,
|
||||
routedChannelType: payload.routedChannelType,
|
||||
inputs: compactInputs.length > 0 ? compactInputs : undefined,
|
||||
inputsOmitted:
|
||||
rawInputs.length > SLACK_INTERACTION_COMPACT_INPUTS_MAX_ITEMS
|
||||
? rawInputs.length - SLACK_INTERACTION_COMPACT_INPUTS_MAX_ITEMS
|
||||
: undefined,
|
||||
payloadTruncated: true,
|
||||
};
|
||||
}
|
||||
|
||||
function formatSlackInteractionSystemEvent(payload: Record<string, unknown>): string {
|
||||
const toEventText = (value: Record<string, unknown>): string =>
|
||||
`${SLACK_INTERACTION_EVENT_PREFIX}${JSON.stringify(value)}`;
|
||||
|
||||
const sanitizedPayload =
|
||||
(sanitizeSlackInteractionPayloadValue(payload) as Record<string, unknown> | undefined) ?? {};
|
||||
let eventText = toEventText(sanitizedPayload);
|
||||
if (eventText.length <= SLACK_INTERACTION_EVENT_MAX_CHARS) {
|
||||
return eventText;
|
||||
}
|
||||
|
||||
const compactPayload = sanitizeSlackInteractionPayloadValue(
|
||||
buildCompactSlackInteractionPayload(sanitizedPayload),
|
||||
) as Record<string, unknown>;
|
||||
eventText = toEventText(compactPayload);
|
||||
if (eventText.length <= SLACK_INTERACTION_EVENT_MAX_CHARS) {
|
||||
return eventText;
|
||||
}
|
||||
|
||||
return toEventText({
|
||||
interactionType: sanitizedPayload.interactionType,
|
||||
actionId: sanitizedPayload.actionId ?? "unknown",
|
||||
userId: sanitizedPayload.userId,
|
||||
channelId: sanitizedPayload.channelId ?? sanitizedPayload.routedChannelId,
|
||||
payloadTruncated: true,
|
||||
});
|
||||
}
|
||||
|
||||
function readOptionValues(options: unknown): string[] | undefined {
|
||||
if (!Array.isArray(options)) {
|
||||
return undefined;
|
||||
|
|
@ -512,7 +664,7 @@ async function emitSlackModalLifecycleEvent(params: {
|
|||
return;
|
||||
}
|
||||
|
||||
enqueueSystemEvent(`Slack interaction: ${JSON.stringify(eventPayload)}`, {
|
||||
enqueueSystemEvent(formatSlackInteractionSystemEvent(eventPayload), {
|
||||
sessionKey: sessionRouting.sessionKey,
|
||||
contextKey: [params.contextPrefix, callbackId, viewId, userId].filter(Boolean).join(":"),
|
||||
});
|
||||
|
|
@ -649,7 +801,7 @@ export function registerSlackInteractionEvents(params: { ctx: SlackMonitorContex
|
|||
const contextParts = ["slack:interaction", channelId, messageTs, actionId].filter(Boolean);
|
||||
const contextKey = contextParts.join(":");
|
||||
|
||||
enqueueSystemEvent(`Slack interaction: ${JSON.stringify(eventPayload)}`, {
|
||||
enqueueSystemEvent(formatSlackInteractionSystemEvent(eventPayload), {
|
||||
sessionKey,
|
||||
contextKey,
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue