mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 22:18:27 +00:00
fix(core): unify session-key normalization and plugin boundary checks
This commit is contained in:
@@ -7,6 +7,7 @@ import { createIMessageTestPlugin } from "../test-utils/imessage-test-plugin.js"
|
||||
import {
|
||||
extractHookToken,
|
||||
isHookAgentAllowed,
|
||||
normalizeHookDispatchSessionKey,
|
||||
resolveHookSessionKey,
|
||||
resolveHookTargetAgentId,
|
||||
normalizeAgentPayload,
|
||||
@@ -280,6 +281,24 @@ describe("gateway hooks helpers", () => {
|
||||
expect(resolvedKey).toEqual({ ok: true, value: "hook:ingress" });
|
||||
});
|
||||
|
||||
test("normalizeHookDispatchSessionKey strips duplicate target agent prefix", () => {
|
||||
expect(
|
||||
normalizeHookDispatchSessionKey({
|
||||
sessionKey: "agent:hooks:slack:channel:c123",
|
||||
targetAgentId: "hooks",
|
||||
}),
|
||||
).toBe("slack:channel:c123");
|
||||
});
|
||||
|
||||
test("normalizeHookDispatchSessionKey preserves non-target agent scoped keys", () => {
|
||||
expect(
|
||||
normalizeHookDispatchSessionKey({
|
||||
sessionKey: "agent:main:slack:channel:c123",
|
||||
targetAgentId: "hooks",
|
||||
}),
|
||||
).toBe("agent:main:slack:channel:c123");
|
||||
});
|
||||
|
||||
test("resolveHooksConfig validates defaultSessionKey and generated fallback against prefixes", () => {
|
||||
expect(() =>
|
||||
resolveHooksConfig({
|
||||
|
||||
@@ -5,7 +5,7 @@ import { listChannelPlugins } from "../channels/plugins/index.js";
|
||||
import type { ChannelId } from "../channels/plugins/types.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { readJsonBodyWithLimit, requestBodyErrorToText } from "../infra/http-body.js";
|
||||
import { normalizeAgentId } from "../routing/session-key.js";
|
||||
import { normalizeAgentId, parseAgentSessionKey } from "../routing/session-key.js";
|
||||
import { normalizeMessageChannel } from "../utils/message-channel.js";
|
||||
import { type HookMappingResolved, resolveHookMappings } from "./hooks-mapping.js";
|
||||
|
||||
@@ -332,6 +332,25 @@ export function resolveHookSessionKey(params: {
|
||||
return { ok: true, value: generated };
|
||||
}
|
||||
|
||||
export function normalizeHookDispatchSessionKey(params: {
|
||||
sessionKey: string;
|
||||
targetAgentId: string | undefined;
|
||||
}): string {
|
||||
const trimmed = params.sessionKey.trim();
|
||||
if (!trimmed || !params.targetAgentId) {
|
||||
return trimmed;
|
||||
}
|
||||
const parsed = parseAgentSessionKey(trimmed);
|
||||
if (!parsed) {
|
||||
return trimmed;
|
||||
}
|
||||
const targetAgentId = normalizeAgentId(params.targetAgentId);
|
||||
if (parsed.agentId !== targetAgentId) {
|
||||
return `agent:${parsed.agentId}:${parsed.rest}`;
|
||||
}
|
||||
return parsed.rest;
|
||||
}
|
||||
|
||||
export function normalizeAgentPayload(payload: Record<string, unknown>):
|
||||
| {
|
||||
ok: true;
|
||||
|
||||
@@ -49,6 +49,7 @@ import {
|
||||
normalizeHookHeaders,
|
||||
normalizeWakePayload,
|
||||
readJsonBody,
|
||||
normalizeHookDispatchSessionKey,
|
||||
resolveHookSessionKey,
|
||||
resolveHookTargetAgentId,
|
||||
resolveHookChannel,
|
||||
@@ -355,10 +356,14 @@ export function createHooksRequestHandler(
|
||||
sendJson(res, 400, { ok: false, error: sessionKey.error });
|
||||
return true;
|
||||
}
|
||||
const targetAgentId = resolveHookTargetAgentId(hooksConfig, normalized.value.agentId);
|
||||
const runId = dispatchAgentHook({
|
||||
...normalized.value,
|
||||
sessionKey: sessionKey.value,
|
||||
agentId: resolveHookTargetAgentId(hooksConfig, normalized.value.agentId),
|
||||
sessionKey: normalizeHookDispatchSessionKey({
|
||||
sessionKey: sessionKey.value,
|
||||
targetAgentId,
|
||||
}),
|
||||
agentId: targetAgentId,
|
||||
});
|
||||
sendJson(res, 202, { ok: true, runId });
|
||||
return true;
|
||||
@@ -408,12 +413,16 @@ export function createHooksRequestHandler(
|
||||
sendJson(res, 400, { ok: false, error: sessionKey.error });
|
||||
return true;
|
||||
}
|
||||
const targetAgentId = resolveHookTargetAgentId(hooksConfig, mapped.action.agentId);
|
||||
const runId = dispatchAgentHook({
|
||||
message: mapped.action.message,
|
||||
name: mapped.action.name ?? "Hook",
|
||||
agentId: resolveHookTargetAgentId(hooksConfig, mapped.action.agentId),
|
||||
agentId: targetAgentId,
|
||||
wakeMode: mapped.action.wakeMode,
|
||||
sessionKey: sessionKey.value,
|
||||
sessionKey: normalizeHookDispatchSessionKey({
|
||||
sessionKey: sessionKey.value,
|
||||
targetAgentId,
|
||||
}),
|
||||
deliver: resolveHookDeliver(mapped.action.deliver),
|
||||
channel,
|
||||
to: mapped.action.to,
|
||||
|
||||
@@ -299,6 +299,48 @@ describe("gateway server hooks", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("normalizes duplicate target-agent prefixes before isolated dispatch", async () => {
|
||||
testState.hooksConfig = {
|
||||
enabled: true,
|
||||
token: "hook-secret",
|
||||
allowRequestSessionKey: true,
|
||||
allowedSessionKeyPrefixes: ["hook:", "agent:"],
|
||||
};
|
||||
testState.agentsConfig = {
|
||||
list: [{ id: "main", default: true }, { id: "hooks" }],
|
||||
};
|
||||
await withGatewayServer(async ({ port }) => {
|
||||
cronIsolatedRun.mockClear();
|
||||
cronIsolatedRun.mockResolvedValueOnce({
|
||||
status: "ok",
|
||||
summary: "done",
|
||||
});
|
||||
|
||||
const resAgent = await fetch(`http://127.0.0.1:${port}/hooks/agent`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: "Bearer hook-secret",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message: "Do it",
|
||||
name: "Email",
|
||||
agentId: "hooks",
|
||||
sessionKey: "agent:hooks:slack:channel:c123",
|
||||
}),
|
||||
});
|
||||
expect(resAgent.status).toBe(202);
|
||||
await waitForSystemEvent();
|
||||
|
||||
const routedCall = (cronIsolatedRun.mock.calls[0] as unknown[] | undefined)?.[0] as
|
||||
| { sessionKey?: string; job?: { agentId?: string } }
|
||||
| undefined;
|
||||
expect(routedCall?.job?.agentId).toBe("hooks");
|
||||
expect(routedCall?.sessionKey).toBe("slack:channel:c123");
|
||||
drainSystemEvents(resolveMainKey());
|
||||
});
|
||||
});
|
||||
|
||||
test("enforces hooks.allowedAgentIds for explicit agent routing", async () => {
|
||||
testState.hooksConfig = {
|
||||
enabled: true,
|
||||
|
||||
@@ -7,7 +7,11 @@ import type { CronJob } from "../../cron/types.js";
|
||||
import { requestHeartbeatNow } from "../../infra/heartbeat-wake.js";
|
||||
import { enqueueSystemEvent } from "../../infra/system-events.js";
|
||||
import type { createSubsystemLogger } from "../../logging/subsystem.js";
|
||||
import type { HookAgentDispatchPayload, HooksConfigResolved } from "../hooks.js";
|
||||
import {
|
||||
normalizeHookDispatchSessionKey,
|
||||
type HookAgentDispatchPayload,
|
||||
type HooksConfigResolved,
|
||||
} from "../hooks.js";
|
||||
import { createHooksRequestHandler } from "../server-http.js";
|
||||
|
||||
type SubsystemLogger = ReturnType<typeof createSubsystemLogger>;
|
||||
@@ -30,7 +34,10 @@ export function createGatewayHooksRequestHandler(params: {
|
||||
};
|
||||
|
||||
const dispatchAgentHook = (value: HookAgentDispatchPayload) => {
|
||||
const sessionKey = value.sessionKey.trim();
|
||||
const sessionKey = normalizeHookDispatchSessionKey({
|
||||
sessionKey: value.sessionKey,
|
||||
targetAgentId: value.agentId,
|
||||
});
|
||||
const mainSessionKey = resolveMainSessionKeyFromConfig();
|
||||
const jobId = randomUUID();
|
||||
const now = Date.now();
|
||||
|
||||
Reference in New Issue
Block a user