mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 19:38:28 +00:00
feat(hooks): add agentId support to webhook mappings (#13672)
* feat(hooks): add agentId support to webhook mappings
Allow webhook mappings to route hook runs to a specific agent via
the new `agentId` field. This enables lightweight agents with minimal
bootstrap files to handle webhooks, reducing token cost per hook run.
The agentId is threaded through:
- HookMappingConfig (config type + zod schema)
- HookMappingResolved + HookAction (mapping types)
- normalizeHookMapping + buildActionFromMapping (mapping logic)
- mergeAction (transform override support)
- HookAgentPayload + normalizeAgentPayload (direct /hooks/agent endpoint)
- dispatchAgentHook → CronJob.agentId (server dispatch)
The existing runCronIsolatedAgentTurn already supports agentId on
CronJob — this change simply wires it through from webhook mappings.
Usage in config:
hooks.mappings[].agentId = "my-agent"
Usage via POST /hooks/agent:
{ "message": "...", "agentId": "my-agent" }
Includes tests for mapping passthrough and payload normalization.
Includes doc updates for webhook.md.
* fix(hooks): enforce webhook agent routing policy + docs/changelog updates (#13672) (thanks @BillChirico)
* fix(hooks): harden explicit agent allowlist semantics (#13672) (thanks @BillChirico)
---------
Co-authored-by: Pip <pip@openclaw.ai>
Co-authored-by: Gustavo Madeira Santana <gumadeiras@gmail.com>
This commit is contained in:
@@ -6,6 +6,8 @@ import { setActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import { createIMessageTestPlugin, createTestRegistry } from "../test-utils/channel-plugins.js";
|
||||
import {
|
||||
extractHookToken,
|
||||
isHookAgentAllowed,
|
||||
resolveHookTargetAgentId,
|
||||
normalizeAgentPayload,
|
||||
normalizeWakePayload,
|
||||
resolveHooksConfig,
|
||||
@@ -126,6 +128,103 @@ describe("gateway hooks helpers", () => {
|
||||
const bad = normalizeAgentPayload({ message: "yo", channel: "sms" });
|
||||
expect(bad.ok).toBe(false);
|
||||
});
|
||||
|
||||
test("normalizeAgentPayload passes agentId", () => {
|
||||
const ok = normalizeAgentPayload(
|
||||
{ message: "hello", agentId: "hooks" },
|
||||
{ idFactory: () => "fixed" },
|
||||
);
|
||||
expect(ok.ok).toBe(true);
|
||||
if (ok.ok) {
|
||||
expect(ok.value.agentId).toBe("hooks");
|
||||
}
|
||||
|
||||
const noAgent = normalizeAgentPayload({ message: "hello" }, { idFactory: () => "fixed" });
|
||||
expect(noAgent.ok).toBe(true);
|
||||
if (noAgent.ok) {
|
||||
expect(noAgent.value.agentId).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
test("resolveHookTargetAgentId falls back to default for unknown agent ids", () => {
|
||||
const cfg = {
|
||||
hooks: { enabled: true, token: "secret" },
|
||||
agents: {
|
||||
list: [{ id: "main", default: true }, { id: "hooks" }],
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
const resolved = resolveHooksConfig(cfg);
|
||||
expect(resolved).not.toBeNull();
|
||||
if (!resolved) {
|
||||
return;
|
||||
}
|
||||
expect(resolveHookTargetAgentId(resolved, "hooks")).toBe("hooks");
|
||||
expect(resolveHookTargetAgentId(resolved, "missing-agent")).toBe("main");
|
||||
expect(resolveHookTargetAgentId(resolved, undefined)).toBeUndefined();
|
||||
});
|
||||
|
||||
test("isHookAgentAllowed honors hooks.allowedAgentIds for explicit routing", () => {
|
||||
const cfg = {
|
||||
hooks: {
|
||||
enabled: true,
|
||||
token: "secret",
|
||||
allowedAgentIds: ["hooks"],
|
||||
},
|
||||
agents: {
|
||||
list: [{ id: "main", default: true }, { id: "hooks" }],
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
const resolved = resolveHooksConfig(cfg);
|
||||
expect(resolved).not.toBeNull();
|
||||
if (!resolved) {
|
||||
return;
|
||||
}
|
||||
expect(isHookAgentAllowed(resolved, undefined)).toBe(true);
|
||||
expect(isHookAgentAllowed(resolved, "hooks")).toBe(true);
|
||||
expect(isHookAgentAllowed(resolved, "missing-agent")).toBe(false);
|
||||
});
|
||||
|
||||
test("isHookAgentAllowed treats empty allowlist as deny-all for explicit agentId", () => {
|
||||
const cfg = {
|
||||
hooks: {
|
||||
enabled: true,
|
||||
token: "secret",
|
||||
allowedAgentIds: [],
|
||||
},
|
||||
agents: {
|
||||
list: [{ id: "main", default: true }, { id: "hooks" }],
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
const resolved = resolveHooksConfig(cfg);
|
||||
expect(resolved).not.toBeNull();
|
||||
if (!resolved) {
|
||||
return;
|
||||
}
|
||||
expect(isHookAgentAllowed(resolved, undefined)).toBe(true);
|
||||
expect(isHookAgentAllowed(resolved, "hooks")).toBe(false);
|
||||
expect(isHookAgentAllowed(resolved, "main")).toBe(false);
|
||||
});
|
||||
|
||||
test("isHookAgentAllowed treats wildcard allowlist as allow-all", () => {
|
||||
const cfg = {
|
||||
hooks: {
|
||||
enabled: true,
|
||||
token: "secret",
|
||||
allowedAgentIds: ["*"],
|
||||
},
|
||||
agents: {
|
||||
list: [{ id: "main", default: true }, { id: "hooks" }],
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
const resolved = resolveHooksConfig(cfg);
|
||||
expect(resolved).not.toBeNull();
|
||||
if (!resolved) {
|
||||
return;
|
||||
}
|
||||
expect(isHookAgentAllowed(resolved, undefined)).toBe(true);
|
||||
expect(isHookAgentAllowed(resolved, "hooks")).toBe(true);
|
||||
expect(isHookAgentAllowed(resolved, "missing-agent")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
const emptyRegistry = createTestRegistry([]);
|
||||
|
||||
Reference in New Issue
Block a user