mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 03:52:42 +00:00
Fix/agent session key normalization (openclaw#15707) thanks @rodrigouroz
Verified: - pnpm build - pnpm check - pnpm test:macmini Co-authored-by: rodrigouroz <384037+rodrigouroz@users.noreply.github.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
@@ -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.
|
- 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/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/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.
|
- 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: 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.
|
- 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.
|
||||||
|
|||||||
@@ -336,4 +336,54 @@ describe("gateway agent handler", () => {
|
|||||||
expect(call?.message).toBe(BARE_SESSION_RESET_PROMPT);
|
expect(call?.message).toBe(BARE_SESSION_RESET_PROMPT);
|
||||||
expect(call?.sessionId).toBe("reset-session-id");
|
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"),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
resolveAgentDeliveryPlan,
|
resolveAgentDeliveryPlan,
|
||||||
resolveAgentOutboundTarget,
|
resolveAgentOutboundTarget,
|
||||||
} from "../../infra/outbound/agent-delivery.js";
|
} 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 { defaultRuntime } from "../../runtime.js";
|
||||||
import { normalizeInputProvenance, type InputProvenance } from "../../sessions/input-provenance.js";
|
import { normalizeInputProvenance, type InputProvenance } from "../../sessions/input-provenance.js";
|
||||||
import { resolveSendPolicy } from "../../sessions/send-policy.js";
|
import { resolveSendPolicy } from "../../sessions/send-policy.js";
|
||||||
@@ -273,6 +273,20 @@ export const agentHandlers: GatewayRequestHandlers = {
|
|||||||
typeof request.sessionKey === "string" && request.sessionKey.trim()
|
typeof request.sessionKey === "string" && request.sessionKey.trim()
|
||||||
? request.sessionKey.trim()
|
? request.sessionKey.trim()
|
||||||
: undefined;
|
: undefined;
|
||||||
|
if (
|
||||||
|
requestedSessionKeyRaw &&
|
||||||
|
classifySessionKeyShape(requestedSessionKeyRaw) === "malformed_agent"
|
||||||
|
) {
|
||||||
|
respond(
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
errorShape(
|
||||||
|
ErrorCodes.INVALID_REQUEST,
|
||||||
|
`invalid agent params: malformed session key "${requestedSessionKeyRaw}"`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
let requestedSessionKey =
|
let requestedSessionKey =
|
||||||
requestedSessionKeyRaw ??
|
requestedSessionKeyRaw ??
|
||||||
resolveExplicitAgentSessionKey({
|
resolveExplicitAgentSessionKey({
|
||||||
@@ -601,6 +615,17 @@ export const agentHandlers: GatewayRequestHandlers = {
|
|||||||
const sessionKeyRaw = typeof p.sessionKey === "string" ? p.sessionKey.trim() : "";
|
const sessionKeyRaw = typeof p.sessionKey === "string" ? p.sessionKey.trim() : "";
|
||||||
let agentId = agentIdRaw ? normalizeAgentId(agentIdRaw) : undefined;
|
let agentId = agentIdRaw ? normalizeAgentId(agentIdRaw) : undefined;
|
||||||
if (sessionKeyRaw) {
|
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);
|
const resolved = resolveAgentIdFromSessionKey(sessionKeyRaw);
|
||||||
if (agentId && resolved !== agentId) {
|
if (agentId && resolved !== agentId) {
|
||||||
respond(
|
respond(
|
||||||
|
|||||||
@@ -285,6 +285,20 @@ describe("gateway server agent", () => {
|
|||||||
expect(spy).not.toHaveBeenCalled();
|
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 () => {
|
test("agent forwards accountId to agentCommand", async () => {
|
||||||
setRegistry(defaultRegistry);
|
setRegistry(defaultRegistry);
|
||||||
testState.allowFrom = ["+1555"];
|
testState.allowFrom = ["+1555"];
|
||||||
|
|||||||
Reference in New Issue
Block a user