diff --git a/src/daemon/webhook.test.ts b/src/daemon/webhook.test.ts index e57365a..f9a5cf9 100644 --- a/src/daemon/webhook.test.ts +++ b/src/daemon/webhook.test.ts @@ -73,13 +73,27 @@ describe("buildWebhookPayload", () => { expect(payload.adapter).toBe("claude-code"); expect(payload.cwd).toBe("/home/user/project"); expect(payload.duration_seconds).toBe(300); - expect(payload.exit_status).toBe("stopped"); + expect(payload.exit_status).toBe(0); expect(payload.summary).toBe("Fix the bug"); expect(payload.meta).toEqual({ openclaw_callback_session_key: "key-1", }); expect(payload.timestamp).toBeTruthy(); }); + + it("prefers explicit exitCode when present", () => { + const payload = buildWebhookPayload({ + id: "sess-456", + adapter: "claude-code", + status: "failed", + startedAt: "2026-03-07T10:00:00.000Z", + stoppedAt: "2026-03-07T10:00:05.000Z", + exitCode: 137, + meta: {}, + }); + + expect(payload.exit_status).toBe(137); + }); }); describe("computeSignature", () => { @@ -126,7 +140,7 @@ describe("emitWebhook", () => { expect(opts.headers["X-Agentctl-Signature"]).toBeUndefined(); }); - it("includes HMAC signature when secret is provided", async () => { + it("includes HMAC signatures when secret is provided", async () => { const payload = buildWebhookPayload({ id: "s1", adapter: "claude-code", @@ -143,6 +157,12 @@ describe("emitWebhook", () => { const [, opts] = (fetch as ReturnType).mock.calls[0]; expect(opts.headers["X-Agentctl-Signature"]).toBeTruthy(); expect(opts.headers["X-Agentctl-Signature"]).toHaveLength(64); + expect(opts.headers["X-Signature"]).toBe( + `sha256=${opts.headers["X-Agentctl-Signature"]}`, + ); + expect(opts.headers["X-Hub-Signature-256"]).toBe( + `sha256=${opts.headers["X-Agentctl-Signature"]}`, + ); }); it("does not throw on fetch failure", async () => { diff --git a/src/daemon/webhook.ts b/src/daemon/webhook.ts index c893122..0c3369b 100644 --- a/src/daemon/webhook.ts +++ b/src/daemon/webhook.ts @@ -12,7 +12,7 @@ export interface WebhookPayload { cwd?: string; adapter: string; duration_seconds: number; - exit_status: string; + exit_status: number; summary?: string; meta: Record; timestamp: string; @@ -47,13 +47,17 @@ export function buildWebhookPayload(session: SessionRecord): WebhookPayload { Math.round((stoppedAt - startedAt) / 1000), ); + const exitStatus = + session.exitCode ?? + (session.status === "error" || session.status === "failed" ? 1 : 0); + return { hook_type: "session.stopped", session_id: session.id, cwd: session.cwd, adapter: session.adapter, duration_seconds: durationSeconds, - exit_status: session.status, + exit_status: exitStatus, summary: session.prompt?.slice(0, 200), meta: session.meta ?? {}, timestamp: new Date().toISOString(), @@ -81,10 +85,10 @@ export async function emitWebhook( }; if (webhookConfig.secret) { - headers["X-Agentctl-Signature"] = computeSignature( - body, - webhookConfig.secret, - ); + const signature = computeSignature(body, webhookConfig.secret); + headers["X-Agentctl-Signature"] = signature; + headers["X-Signature"] = `sha256=${signature}`; + headers["X-Hub-Signature-256"] = `sha256=${signature}`; } // Use global fetch (available in Node 18+)