From b053feedd7df5774739e239d243ffb614894a3db Mon Sep 17 00:00:00 2001 From: Ali Mohammad Date: Mon, 19 Oct 2026 21:50:40 +0300 Subject: [PATCH 1/2] fix(channels): webhookChannel replies with a trimmed body, not the whole ExecutionResult Co-Authored-By: Claude Opus 5.5 --- src/channels/webhookChannel.test.ts | 22 +++++++++++++++++++++- src/channels/webhookChannel.ts | 25 +++++++++++++++++++++---- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/src/channels/webhookChannel.test.ts b/src/channels/webhookChannel.test.ts index 7f43c7d61..bcd1fdc04 100644 --- a/src/channels/webhookChannel.test.ts +++ b/src/channels/webhookChannel.test.ts @@ -117,7 +117,27 @@ describe('webhookChannel auth (test vectors)', () => { expect(res.json).toMatchObject({ finishReason: 'awaiting-approval' }); expect(res.json.approvalPrompt).toContain('Approve restart_service'); expect(res.json.approvalPrompt).toContain('checkout'); - expect(res.json.approvalPrompt).toContain(String(res.json.approvalId)); + expect(res.json.approvalPrompt).toContain((res.json.approval as { id: string }).id); + expect(res.json).not.toHaveProperty('messages'); + }); + + it('replies with a trimmed body by default: no transcript, system prompt or usage', async () => { + const agent = createAgent({ provider: mockModel(['hi there']), instructions: 'SECRET-SYSTEM-PROMPT' }); + const res = await viaChannel(webhookChannel({}), body, {}, agent); + + expect(res.status).toBe(200); + expect(res.json).toMatchObject({ text: 'hi there', finishReason: 'stop' }); + expect(typeof res.json.sessionId).toBe('string'); + for (const key of ['messages', 'stepUsage', 'usage', 'toolCalls']) expect(res.json).not.toHaveProperty(key); + expect(JSON.stringify(res.json)).not.toContain('SECRET-SYSTEM-PROMPT'); + }); + + it('`fullResult: true` restores the whole ExecutionResult', async () => { + const agent = createAgent({ provider: mockModel(['hi there']), instructions: 'SECRET-SYSTEM-PROMPT' }); + const res = await viaChannel(webhookChannel({ fullResult: true }), body, {}, agent); + + expect(res.json).toHaveProperty('messages'); + expect(JSON.stringify(res.json)).toContain('SECRET-SYSTEM-PROMPT'); }); it('a body without what `principal` needs runs without one', async () => { diff --git a/src/channels/webhookChannel.ts b/src/channels/webhookChannel.ts index 33c17c000..543226eff 100644 --- a/src/channels/webhookChannel.ts +++ b/src/channels/webhookChannel.ts @@ -23,6 +23,14 @@ export interface WebhookChannelOptions { * `req` carries the headers. Set it only from data `auth` vouched for. */ principal?: (body: unknown, req: ChannelRequest) => Principal | undefined; + /** + * Reply with the turn's whole `ExecutionResult` (messages, tool calls, + * usage) instead of the trimmed default `{ sessionId, text, finishReason, + * approval }`. The full result carries the system prompt and the whole + * transcript, so enable it only when every caller is trusted with them. + * Default `false`. + */ + fullResult?: boolean; } /** A webhook channel: `verify` (set when `secret` or `auth` is) resolves a `ChannelAuthResult`. */ @@ -64,7 +72,8 @@ async function verifyWebhook(auth: WebhookAuth | undefined, req: ChannelRequest) /** * A generic webhook: the body's JSON `input` string (or the whole body) is - * the input, and the response is the turn's `ExecutionResult` as JSON. Each request is a one-shot session unless the + * the input, and the response is JSON `{ sessionId, text, finishReason, approval? }` (the whole + * `ExecutionResult` with `fullResult: true`). Each request is a one-shot session unless the * JSON body has a `sessionKey` string. With `secret` or `auth`, a request * that fails the check gets 401. `principal` names the caller of the turn * (an alerting system, an integration user) from the verified request. @@ -96,12 +105,20 @@ export function webhookChannel(options: WebhookChannelOptions = {}): WebhookChan const principal = options.principal?.(body, req); return { sessionKey: typeof sessionKey === 'string' && sessionKey ? sessionKey : newId(), input, ...(principal && { principal }), replyTo: null }; }, - async reply({ text, result, respond }) { + async reply({ sessionId, text, result, approval, approvals, respond }) { // On an approval pause `result.text` is empty (the last turn was a bare // tool call), so the human-readable prompt the channel computed is sent // too: a consumer can render it instead of rebuilding it from `approvalId`. - if (result?.finishReason === 'awaiting-approval') return respond?.(200, { ...result, approvalPrompt: text }); - respond?.(200, result ?? { text }); + const paused = result?.finishReason === 'awaiting-approval'; + if (options.fullResult) return respond?.(200, paused ? { ...result, approvalPrompt: text } : (result ?? { text })); + respond?.(200, { + sessionId, + text, + finishReason: result?.finishReason, + ...(approval && { approval }), + ...(approvals && { approvals }), + ...(paused && { approvalPrompt: text }), + }); }, }; defineChannel(channel); From 050ab71630c91d042ea77ba682b9027340f58fe9 Mon Sep 17 00:00:00 2001 From: Ali Mohammad Date: Mon, 19 Oct 2026 23:03:43 +0300 Subject: [PATCH 2/2] chore(api): refresh index API report for webhookChannel fullResult Co-Authored-By: Claude Opus 5.5 --- api/index.api.md | 1 + 1 file changed, 1 insertion(+) diff --git a/api/index.api.md b/api/index.api.md index 874f3a183..755040a3a 100644 --- a/api/index.api.md +++ b/api/index.api.md @@ -7161,6 +7161,7 @@ export function webhookChannel(options?: WebhookChannelOptions): WebhookChannel; // @public export interface WebhookChannelOptions { auth?: WebhookAuth; + fullResult?: boolean; name?: string; principal?: (body: unknown, req: ChannelRequest) => Principal | undefined; secret?: string;