mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
fix: harden msteams revoked-context fallback delivery (#27224) (thanks @openperf)
This commit is contained in:
@@ -991,6 +991,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Security/MSTeams media: enforce allowlist checks for SharePoint reference attachment URLs and redirect targets during Graph-backed media fetches so redirect chains cannot escape configured media host boundaries. Thanks @tdjackey for reporting.
|
||||
- Security/MSTeams media: route attachment auth-retry and Graph SharePoint download redirects through shared `safeFetch` so each hop is validated with allowlist + DNS/IP checks across the full redirect chain. (#23598) Thanks @Asm3r96 and @lewiswigmore.
|
||||
- Security/MSTeams auth redirect scoping: strip bearer auth on redirect hops outside `authAllowHosts` and gate SharePoint Graph auth-header injection by auth allowlist to prevent token bleed across redirect targets. (#25045) Thanks @bmendonca3.
|
||||
- MSTeams/reply reliability: when Bot Framework revokes thread turn-context proxies (for example debounced flush paths), fall back to proactive messaging/typing and continue pending sends without duplicating already delivered messages. (#27224) Thanks @openperf.
|
||||
- Security/macOS discovery: fail closed for unresolved discovery endpoints by clearing stale remote selection values, use resolved service host only for SSH target derivation, and keep remote URL config aligned with resolved endpoint availability. (#21618) Thanks @bmendonca3.
|
||||
- Chat/Usage/TUI: strip synthetic inbound metadata blocks (including `Conversation info` and trailing `Untrusted context` channel metadata wrappers) from displayed conversation history so internal prompt context no longer leaks into user-visible logs.
|
||||
- CI/Tests: fix TypeScript case-table typing and lint assertion regressions so `pnpm check` passes again after Synology Chat landing. (#23012) Thanks @druide67.
|
||||
|
||||
@@ -323,6 +323,47 @@ describe("msteams messenger", () => {
|
||||
expect(ids).toEqual(["id:hello"]);
|
||||
});
|
||||
|
||||
it("falls back only for remaining thread messages after context revocation", async () => {
|
||||
const threadSent: string[] = [];
|
||||
const proactiveSent: string[] = [];
|
||||
let attempt = 0;
|
||||
|
||||
const ctx = {
|
||||
sendActivity: async (activity: unknown) => {
|
||||
const { text } = activity as { text?: string };
|
||||
const content = text ?? "";
|
||||
attempt += 1;
|
||||
if (attempt === 1) {
|
||||
threadSent.push(content);
|
||||
return { id: `id:${content}` };
|
||||
}
|
||||
throw new TypeError("Cannot perform 'set' on a proxy that has been revoked");
|
||||
},
|
||||
};
|
||||
|
||||
const adapter: MSTeamsAdapter = {
|
||||
continueConversation: async (_appId, _reference, logic) => {
|
||||
await logic({
|
||||
sendActivity: createRecordedSendActivity(proactiveSent),
|
||||
});
|
||||
},
|
||||
process: async () => {},
|
||||
};
|
||||
|
||||
const ids = await sendMSTeamsMessages({
|
||||
replyStyle: "thread",
|
||||
adapter,
|
||||
appId: "app123",
|
||||
conversationRef: baseRef,
|
||||
context: ctx,
|
||||
messages: [{ text: "one" }, { text: "two" }, { text: "three" }],
|
||||
});
|
||||
|
||||
expect(threadSent).toEqual(["one"]);
|
||||
expect(proactiveSent).toEqual(["two", "three"]);
|
||||
expect(ids).toEqual(["id:one", "id:two", "id:three"]);
|
||||
});
|
||||
|
||||
it("retries top-level sends on transient (5xx)", async () => {
|
||||
const attempts: string[] = [];
|
||||
|
||||
|
||||
@@ -441,9 +441,13 @@ export async function sendMSTeamsMessages(params: {
|
||||
}
|
||||
};
|
||||
|
||||
const sendMessagesInContext = async (ctx: SendContext): Promise<string[]> => {
|
||||
const sendMessagesInContext = async (
|
||||
ctx: SendContext,
|
||||
batch: MSTeamsRenderedMessage[] = messages,
|
||||
offset = 0,
|
||||
): Promise<string[]> => {
|
||||
const messageIds: string[] = [];
|
||||
for (const [idx, message] of messages.entries()) {
|
||||
for (const [idx, message] of batch.entries()) {
|
||||
const response = await sendWithRetry(
|
||||
async () =>
|
||||
await ctx.sendActivity(
|
||||
@@ -455,38 +459,52 @@ export async function sendMSTeamsMessages(params: {
|
||||
params.mediaMaxBytes,
|
||||
),
|
||||
),
|
||||
{ messageIndex: idx, messageCount: messages.length },
|
||||
{ messageIndex: offset + idx, messageCount: messages.length },
|
||||
);
|
||||
messageIds.push(extractMessageId(response) ?? "unknown");
|
||||
}
|
||||
return messageIds;
|
||||
};
|
||||
|
||||
const sendProactively = async (
|
||||
batch: MSTeamsRenderedMessage[] = messages,
|
||||
offset = 0,
|
||||
): Promise<string[]> => {
|
||||
const baseRef = buildConversationReference(params.conversationRef);
|
||||
const proactiveRef: MSTeamsConversationReference = {
|
||||
...baseRef,
|
||||
activityId: undefined,
|
||||
};
|
||||
|
||||
const messageIds: string[] = [];
|
||||
await params.adapter.continueConversation(params.appId, proactiveRef, async (ctx) => {
|
||||
messageIds.push(...(await sendMessagesInContext(ctx, batch, offset)));
|
||||
});
|
||||
return messageIds;
|
||||
};
|
||||
|
||||
if (params.replyStyle === "thread") {
|
||||
const ctx = params.context;
|
||||
if (!ctx) {
|
||||
throw new Error("Missing context for replyStyle=thread");
|
||||
}
|
||||
try {
|
||||
return await sendMessagesInContext(ctx);
|
||||
} catch (err) {
|
||||
if (!isRevokedProxyError(err)) {
|
||||
throw err;
|
||||
const messageIds: string[] = [];
|
||||
for (const [idx, message] of messages.entries()) {
|
||||
try {
|
||||
messageIds.push(...(await sendMessagesInContext(ctx, [message], idx)));
|
||||
} catch (err) {
|
||||
if (!isRevokedProxyError(err)) {
|
||||
throw err;
|
||||
}
|
||||
const remaining = messages.slice(idx);
|
||||
if (remaining.length > 0) {
|
||||
messageIds.push(...(await sendProactively(remaining, idx)));
|
||||
}
|
||||
return messageIds;
|
||||
}
|
||||
// Turn context revoked (debounced message) — fall back to proactive
|
||||
// messaging so the reply still reaches the user.
|
||||
}
|
||||
return messageIds;
|
||||
}
|
||||
|
||||
const baseRef = buildConversationReference(params.conversationRef);
|
||||
const proactiveRef: MSTeamsConversationReference = {
|
||||
...baseRef,
|
||||
activityId: undefined,
|
||||
};
|
||||
|
||||
const messageIds: string[] = [];
|
||||
await params.adapter.continueConversation(params.appId, proactiveRef, async (ctx) => {
|
||||
messageIds.push(...(await sendMessagesInContext(ctx)));
|
||||
});
|
||||
return messageIds;
|
||||
return await sendProactively(messages, 0);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user