diff --git a/src/config/sessions.ts b/src/config/sessions.ts index e5223bdbf..31ae326bb 100644 --- a/src/config/sessions.ts +++ b/src/config/sessions.ts @@ -62,11 +62,9 @@ export async function saveSessionStore( store: Record, ) { await fs.promises.mkdir(path.dirname(storePath), { recursive: true }); - await fs.promises.writeFile( - storePath, - JSON.stringify(store, null, 2), - "utf-8", - ); + const tmp = `${storePath}.${process.pid}.${crypto.randomUUID()}.tmp`; + await fs.promises.writeFile(tmp, JSON.stringify(store, null, 2), "utf-8"); + await fs.promises.rename(tmp, storePath); } export async function updateLastRoute(params: { diff --git a/src/gateway/server.test.ts b/src/gateway/server.test.ts index d5a1bc7b5..0dbd41a9f 100644 --- a/src/gateway/server.test.ts +++ b/src/gateway/server.test.ts @@ -11,9 +11,11 @@ import { GatewayLockError } from "../infra/gateway-lock.js"; import { startGatewayServer } from "./server.js"; let testSessionStorePath: string | undefined; +let testAllowFrom: string[] | undefined; vi.mock("../config/config.js", () => ({ loadConfig: () => ({ inbound: { + allowFrom: testAllowFrom, reply: { mode: "command", command: ["echo", "ok"], @@ -108,7 +110,69 @@ async function startServerWithClient(token?: string) { } describe("gateway server", () => { + test("agent falls back to allowFrom when lastTo is stale", async () => { + testAllowFrom = ["+436769770569"]; + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdis-gw-")); + testSessionStorePath = path.join(dir, "sessions.json"); + await fs.writeFile( + testSessionStorePath, + JSON.stringify( + { + main: { + sessionId: "sess-main-stale", + updatedAt: Date.now(), + lastChannel: "whatsapp", + lastTo: "+1555", + }, + }, + null, + 2, + ), + "utf-8", + ); + + const { server, ws } = await startServerWithClient(); + ws.send( + JSON.stringify({ + type: "hello", + minProtocol: 1, + maxProtocol: 1, + client: { name: "test", version: "1", platform: "test", mode: "test" }, + caps: [], + }), + ); + await onceMessage(ws, (o) => o.type === "hello-ok"); + + ws.send( + JSON.stringify({ + type: "req", + id: "agent-last-stale", + method: "agent", + params: { + message: "hi", + sessionKey: "main", + channel: "last", + deliver: true, + idempotencyKey: "idem-agent-last-stale", + }, + }), + ); + await onceMessage(ws, (o) => o.type === "res" && o.id === "agent-last-stale"); + + const spy = vi.mocked(agentCommand); + expect(spy).toHaveBeenCalled(); + const call = spy.mock.calls.at(-1)?.[0] as Record; + expect(call.provider).toBe("whatsapp"); + expect(call.to).toBe("+436769770569"); + expect(call.sessionId).toBe("sess-main-stale"); + + ws.close(); + await server.close(); + testAllowFrom = undefined; + }); + test("agent routes main last-channel whatsapp", async () => { + testAllowFrom = undefined; const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdis-gw-")); testSessionStorePath = path.join(dir, "sessions.json"); await fs.writeFile( diff --git a/src/gateway/server.ts b/src/gateway/server.ts index bd645e30f..06d5ec295 100644 --- a/src/gateway/server.ts +++ b/src/gateway/server.ts @@ -36,6 +36,7 @@ import { monitorTelegramProvider } from "../telegram/monitor.js"; import { sendMessageTelegram } from "../telegram/send.js"; import { sendMessageWhatsApp } from "../web/outbound.js"; import { ensureWebChatServerFromConfig } from "../webchat/server.js"; +import { normalizeE164 } from "../utils.js"; import { buildMessageWithAttachments } from "./chat-attachments.js"; import { ErrorCodes, @@ -1132,10 +1133,12 @@ export async function startGatewayServer( let resolvedSessionId = params.sessionId?.trim() || undefined; let sessionEntry: SessionEntry | undefined; let bestEffortDeliver = false; + let cfgForAgent: ReturnType | undefined; if (requestedSessionKey) { const { cfg, storePath, store, entry } = loadSessionEntry(requestedSessionKey); + cfgForAgent = cfg; const now = Date.now(); const sessionId = entry?.sessionId ?? randomUUID(); sessionEntry = { @@ -1206,6 +1209,35 @@ export async function startGatewayServer( return undefined; })(); + const sanitizedTo = (() => { + // If we derived a WhatsApp recipient from session "lastTo", ensure it is still valid + // for the configured allowlist. Otherwise, fall back to the first allowed number so + // voice wake doesn't silently route to stale/test recipients. + if (resolvedChannel !== "whatsapp") return resolvedTo; + const explicit = + typeof params.to === "string" && params.to.trim() + ? params.to.trim() + : undefined; + if (explicit) return resolvedTo; + + const cfg = cfgForAgent ?? loadConfig(); + const rawAllow = cfg.inbound?.allowFrom ?? []; + if (rawAllow.includes("*")) return resolvedTo; + const allowFrom = rawAllow + .map((val) => normalizeE164(val)) + .filter((val) => val.length > 1); + if (allowFrom.length === 0) return resolvedTo; + + const normalizedLast = + typeof resolvedTo === "string" && resolvedTo.trim() + ? normalizeE164(resolvedTo) + : undefined; + if (normalizedLast && allowFrom.includes(normalizedLast)) { + return normalizedLast; + } + return allowFrom[0]; + })(); + const deliver = params.deliver === true && resolvedChannel !== "webchat"; @@ -1221,7 +1253,7 @@ export async function startGatewayServer( void agentCommand( { message, - to: resolvedTo, + to: sanitizedTo, sessionId: resolvedSessionId, thinking: params.thinking, deliver,