diff --git a/CHANGELOG.md b/CHANGELOG.md index 9021f6596aa..d271b6756af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Docs: https://docs.openclaw.ai - Auto-reply/WhatsApp/TUI/Web: when a final assistant message is `NO_REPLY` and a messaging tool send succeeded, mirror the delivered messaging-tool text into session-visible assistant output so TUI/Web no longer show `NO_REPLY` placeholders. (#7010) Thanks @Morrowind-Xie. - Gateway/Chat: harden `chat.send` inbound message handling by rejecting null bytes, stripping unsafe control characters, and normalizing Unicode to NFC before dispatch. (#8593) Thanks @fr33d3m0n. - Gateway/Send: return an actionable error when `send` targets internal-only `webchat`, guiding callers to use `chat.send` or a deliverable channel. (#15703) Thanks @rodrigouroz. +- Gateway/Agent: reject malformed `agent:`-prefixed session keys (for example, `agent:main`) in `agent` and `agent.identity.get` instead of silently resolving them to the default agent, preventing accidental cross-session routing. (#15707) Thanks @rodrigouroz. - Gateway/Security: redact sensitive session/path details from `status` responses for non-admin clients; full details remain available to `operator.admin`. (#8590) Thanks @fr33d3m0n. - Agents: return an explicit timeout error reply when an embedded run times out before producing any payloads, preventing silent dropped turns during slow cache-refresh transitions. (#16659) Thanks @liaosvcaf and @vignesh07. - Agents/OpenAI: force `store=true` for direct OpenAI Responses/Codex runs to preserve multi-turn server-side conversation state, while leaving proxy/non-OpenAI endpoints unchanged. (#16803) Thanks @mark9232 and @vignesh07. diff --git a/src/gateway/server-methods/agent.test.ts b/src/gateway/server-methods/agent.test.ts index 6286e356db0..bdb051e00e5 100644 --- a/src/gateway/server-methods/agent.test.ts +++ b/src/gateway/server-methods/agent.test.ts @@ -336,4 +336,54 @@ describe("gateway agent handler", () => { expect(call?.message).toBe(BARE_SESSION_RESET_PROMPT); expect(call?.sessionId).toBe("reset-session-id"); }); + + it("rejects malformed agent session keys early in agent handler", async () => { + mocks.agentCommand.mockClear(); + const respond = vi.fn(); + + await agentHandlers.agent({ + params: { + message: "test", + sessionKey: "agent:main", + idempotencyKey: "test-malformed-session-key", + }, + respond, + context: makeContext(), + req: { type: "req", id: "4", method: "agent" }, + client: null, + isWebchatConnect: () => false, + }); + + expect(mocks.agentCommand).not.toHaveBeenCalled(); + expect(respond).toHaveBeenCalledWith( + false, + undefined, + expect.objectContaining({ + message: expect.stringContaining("malformed session key"), + }), + ); + }); + + it("rejects malformed session keys in agent.identity.get", async () => { + const respond = vi.fn(); + + await agentHandlers["agent.identity.get"]({ + params: { + sessionKey: "agent:main", + }, + respond, + context: makeContext(), + req: { type: "req", id: "5", method: "agent.identity.get" }, + client: null, + isWebchatConnect: () => false, + }); + + expect(respond).toHaveBeenCalledWith( + false, + undefined, + expect.objectContaining({ + message: expect.stringContaining("malformed session key"), + }), + ); + }); }); diff --git a/src/gateway/server-methods/agent.ts b/src/gateway/server-methods/agent.ts index 5d4c8773fab..d9c3451ee9b 100644 --- a/src/gateway/server-methods/agent.ts +++ b/src/gateway/server-methods/agent.ts @@ -16,7 +16,7 @@ import { resolveAgentDeliveryPlan, resolveAgentOutboundTarget, } from "../../infra/outbound/agent-delivery.js"; -import { normalizeAgentId } from "../../routing/session-key.js"; +import { classifySessionKeyShape, normalizeAgentId } from "../../routing/session-key.js"; import { defaultRuntime } from "../../runtime.js"; import { normalizeInputProvenance, type InputProvenance } from "../../sessions/input-provenance.js"; import { resolveSendPolicy } from "../../sessions/send-policy.js"; @@ -273,6 +273,20 @@ export const agentHandlers: GatewayRequestHandlers = { typeof request.sessionKey === "string" && request.sessionKey.trim() ? request.sessionKey.trim() : undefined; + if ( + requestedSessionKeyRaw && + classifySessionKeyShape(requestedSessionKeyRaw) === "malformed_agent" + ) { + respond( + false, + undefined, + errorShape( + ErrorCodes.INVALID_REQUEST, + `invalid agent params: malformed session key "${requestedSessionKeyRaw}"`, + ), + ); + return; + } let requestedSessionKey = requestedSessionKeyRaw ?? resolveExplicitAgentSessionKey({ @@ -601,6 +615,17 @@ export const agentHandlers: GatewayRequestHandlers = { const sessionKeyRaw = typeof p.sessionKey === "string" ? p.sessionKey.trim() : ""; let agentId = agentIdRaw ? normalizeAgentId(agentIdRaw) : undefined; if (sessionKeyRaw) { + if (classifySessionKeyShape(sessionKeyRaw) === "malformed_agent") { + respond( + false, + undefined, + errorShape( + ErrorCodes.INVALID_REQUEST, + `invalid agent.identity.get params: malformed session key "${sessionKeyRaw}"`, + ), + ); + return; + } const resolved = resolveAgentIdFromSessionKey(sessionKeyRaw); if (agentId && resolved !== agentId) { respond( diff --git a/src/gateway/server.agent.gateway-server-agent-a.e2e.test.ts b/src/gateway/server.agent.gateway-server-agent-a.e2e.test.ts index d783e42d531..ed6b9d4cce2 100644 --- a/src/gateway/server.agent.gateway-server-agent-a.e2e.test.ts +++ b/src/gateway/server.agent.gateway-server-agent-a.e2e.test.ts @@ -285,6 +285,20 @@ describe("gateway server agent", () => { expect(spy).not.toHaveBeenCalled(); }); + test("agent rejects malformed agent-prefixed session keys", async () => { + setRegistry(defaultRegistry); + const res = await rpcReq(ws, "agent", { + message: "hi", + sessionKey: "agent:main", + idempotencyKey: "idem-agent-malformed-key", + }); + expect(res.ok).toBe(false); + expect(res.error?.message).toContain("malformed session key"); + + const spy = vi.mocked(agentCommand); + expect(spy).not.toHaveBeenCalled(); + }); + test("agent forwards accountId to agentCommand", async () => { setRegistry(defaultRegistry); testState.allowFrom = ["+1555"];