From 8ab20d27dd8f63e708ac609de2bb1f66043198f6 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Fri, 13 Nov 2026 06:30:21 +0800 Subject: [PATCH] fix(chat): verify bound history source before publishing Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../dashboard/smoke/bound-conversation-smoke.ts | 14 ++++++++++++++ apps/presentation/dashboard/src/data/chat.ts | 7 +++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/apps/presentation/dashboard/smoke/bound-conversation-smoke.ts b/apps/presentation/dashboard/smoke/bound-conversation-smoke.ts index b7a61c4567..b0c3e86794 100644 --- a/apps/presentation/dashboard/smoke/bound-conversation-smoke.ts +++ b/apps/presentation/dashboard/smoke/bound-conversation-smoke.ts @@ -187,6 +187,20 @@ try { assert.deepEqual(calls, [{path: `/api/chat/sessions?conversation_binding_id=${bindingId}`, method: "GET"}], "A fresh check of the same unchanged binding reuses the complete verified history"); + // The final source check can keep the same address while reporting a + // different canonical Session scope. Previously read pages cannot publish. + sourceChecks = 0; + globalThis.fetch = async (input, init) => { + const response = await routedRead(input, init); + if (!isBoundDiscovery(input) || ++sourceChecks !== 2) return response; + const changedSource = await response.json(); + changedSource.sessions[0].channel_id = "unrelated-original-channel"; + return new Response(JSON.stringify(changedSource), {status: response.status}); + }; + await assert.rejects(() => fetchChatHistory(options), /original Session scope changed during history read/); + assert.equal(sourceChecks, 2); + globalThis.fetch = routedRead; + // Current source revocation blocks recovery before another history page. child.stdin.write("deny\n"); assert.equal((await next()).source_verified, false); calls.length = 0; diff --git a/apps/presentation/dashboard/src/data/chat.ts b/apps/presentation/dashboard/src/data/chat.ts index 95f19d780f..1111dd55d9 100644 --- a/apps/presentation/dashboard/src/data/chat.ts +++ b/apps/presentation/dashboard/src/data/chat.ts @@ -1375,10 +1375,13 @@ export async function fetchChatHistory(options: { // while it was in flight. Reuse the original source owner, never a fallback. if (missing.length && results.some(result => result.status === "fulfilled")) { if (options.bound) { + const read = results[0]; try { - await resolveBoundChatSession(options.bound, AbortSignal.timeout(CHAT_HISTORY_READ_TIMEOUT_MS)); + const confirmed = await resolveBoundChatSession(options.bound, AbortSignal.timeout(CHAT_HISTORY_READ_TIMEOUT_MS)); + if (read?.status === "fulfilled" && !sameHistorySource(confirmed, read.value.session)) { + throw new Error("The original Session scope changed during history read."); + } } catch (error) { - const read = results[0]; if (options.managed || read?.status !== "fulfilled" || !historyTransportUnavailable(error)) throw error; return { messages: [],