diff --git a/CHANGELOG.md b/CHANGELOG.md index 45616ac2ac8..168ed898a6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ Docs: https://docs.openclaw.ai - Gateway/Control UI basePath webhook passthrough: let non-read methods under configured `controlUiBasePath` fall through to plugin routes (instead of returning Control UI 405), restoring webhook handlers behind basePath mounts. (#32311) Thanks @ademczuk. - Voice-call/Twilio signature verification: retry signature validation across deterministic URL port variants (with/without port) to handle mixed Twilio signing behavior behind reverse proxies and non-standard ports. (#25140) Thanks @drvoss. +- Hooks/webhook ACK compatibility: return `200` (instead of `202`) for successful `/hooks/agent` requests so providers that require `200` (for example Forward Email) accept dispatched agent hook deliveries. (#28204) Thanks @Glucksberg. - Voice-call/Twilio external outbound: auto-register webhook-first `outbound-api` calls (initiated outside OpenClaw) so media streams are accepted and call direction metadata stays accurate. (#31181) Thanks @scoootscooob. - Voice-call/Twilio inbound greeting: run answered-call initial notify greeting for Twilio instead of skipping the manager speak path, with regression coverage for both Twilio and Plivo notify flows. (#29121) Thanks @xinhuagu. - Voice-call/stale call hydration: verify active calls with the provider before loading persisted in-progress calls so stale locally persisted records do not block or misroute new call handling after restarts. (#4325) Thanks @garnetlyx. diff --git a/docs/automation/webhook.md b/docs/automation/webhook.md index 8072b4a1a3f..b35ee9d4469 100644 --- a/docs/automation/webhook.md +++ b/docs/automation/webhook.md @@ -159,7 +159,7 @@ Mapping options (summary): ## Responses - `200` for `/hooks/wake` -- `202` for `/hooks/agent` (async run started) +- `200` for `/hooks/agent` (async run accepted) - `401` on auth failure - `429` after repeated auth failures from the same client (check `Retry-After`) - `400` on invalid payload diff --git a/src/gateway/server-http.ts b/src/gateway/server-http.ts index f16bf6d8a51..ef0e56dd6d9 100644 --- a/src/gateway/server-http.ts +++ b/src/gateway/server-http.ts @@ -358,7 +358,7 @@ export function createHooksRequestHandler( }), agentId: targetAgentId, }); - sendJson(res, 202, { ok: true, runId }); + sendJson(res, 200, { ok: true, runId }); return true; } @@ -424,7 +424,7 @@ export function createHooksRequestHandler( timeoutSeconds: mapped.action.timeoutSeconds, allowUnsafeExternalContent: mapped.action.allowUnsafeExternalContent, }); - sendJson(res, 202, { ok: true, runId }); + sendJson(res, 200, { ok: true, runId }); return true; } } catch (err) { diff --git a/src/gateway/server.hooks.test.ts b/src/gateway/server.hooks.test.ts index ddad8bd6dc9..0c125600f5d 100644 --- a/src/gateway/server.hooks.test.ts +++ b/src/gateway/server.hooks.test.ts @@ -72,7 +72,7 @@ describe("gateway server hooks", () => { mockIsolatedRunOkOnce(); const resAgent = await postHook(port, "/hooks/agent", { message: "Do it", name: "Email" }); - expect(resAgent.status).toBe(202); + expect(resAgent.status).toBe(200); const agentEvents = await waitForSystemEvent(); expect(agentEvents.some((e) => e.includes("Hook Email: done"))).toBe(true); drainSystemEvents(resolveMainKey()); @@ -83,7 +83,7 @@ describe("gateway server hooks", () => { name: "Email", model: "openai/gpt-4.1-mini", }); - expect(resAgentModel.status).toBe(202); + expect(resAgentModel.status).toBe(200); await waitForSystemEvent(); const call = (cronIsolatedRun.mock.calls[0] as unknown[] | undefined)?.[0] as { job?: { payload?: { model?: string } }; @@ -97,7 +97,7 @@ describe("gateway server hooks", () => { name: "Email", agentId: "hooks", }); - expect(resAgentWithId.status).toBe(202); + expect(resAgentWithId.status).toBe(200); await waitForSystemEvent(); const routedCall = (cronIsolatedRun.mock.calls[0] as unknown[] | undefined)?.[0] as { job?: { agentId?: string }; @@ -111,7 +111,7 @@ describe("gateway server hooks", () => { name: "Email", agentId: "missing-agent", }); - expect(resAgentUnknown.status).toBe(202); + expect(resAgentUnknown.status).toBe(200); await waitForSystemEvent(); const fallbackCall = (cronIsolatedRun.mock.calls[0] as unknown[] | undefined)?.[0] as { job?: { agentId?: string }; @@ -201,8 +201,15 @@ describe("gateway server hooks", () => { cronIsolatedRun.mockClear(); cronIsolatedRun.mockResolvedValue({ status: "ok", summary: "done" }); - const defaultRoute = await postHook(port, "/hooks/agent", { message: "No key" }); - expect(defaultRoute.status).toBe(202); + const defaultRoute = 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: "No key" }), + }); + expect(defaultRoute.status).toBe(200); await waitForSystemEvent(); const defaultCall = (cronIsolatedRun.mock.calls[0] as unknown[] | undefined)?.[0] as | { sessionKey?: string } @@ -212,8 +219,15 @@ describe("gateway server hooks", () => { cronIsolatedRun.mockClear(); cronIsolatedRun.mockResolvedValue({ status: "ok", summary: "done" }); - const mappedOk = await postHook(port, "/hooks/mapped-ok", { subject: "hello", id: "42" }); - expect(mappedOk.status).toBe(202); + const mappedOk = await fetch(`http://127.0.0.1:${port}/hooks/mapped-ok`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer hook-secret", + }, + body: JSON.stringify({ subject: "hello", id: "42" }), + }); + expect(mappedOk.status).toBe(200); await waitForSystemEvent(); const mappedCall = (cronIsolatedRun.mock.calls[0] as unknown[] | undefined)?.[0] as | { sessionKey?: string } @@ -249,7 +263,7 @@ describe("gateway server hooks", () => { agentId: "hooks", sessionKey: "agent:hooks:slack:channel:c123", }); - expect(resAgent.status).toBe(202); + expect(resAgent.status).toBe(200); await waitForSystemEvent(); const routedCall = (cronIsolatedRun.mock.calls[0] as unknown[] | undefined)?.[0] as @@ -279,7 +293,7 @@ describe("gateway server hooks", () => { await withGatewayServer(async ({ port }) => { mockIsolatedRunOkOnce(); const resNoAgent = await postHook(port, "/hooks/agent", { message: "No explicit agent" }); - expect(resNoAgent.status).toBe(202); + expect(resNoAgent.status).toBe(200); await waitForSystemEvent(); const noAgentCall = (cronIsolatedRun.mock.calls[0] as unknown[] | undefined)?.[0] as { job?: { agentId?: string }; @@ -292,7 +306,7 @@ describe("gateway server hooks", () => { message: "Allowed", agentId: "hooks", }); - expect(resAllowed.status).toBe(202); + expect(resAllowed.status).toBe(200); await waitForSystemEvent(); const allowedCall = (cronIsolatedRun.mock.calls[0] as unknown[] | undefined)?.[0] as { job?: { agentId?: string };