From 9c2fbc7218f6bb2dbfc04242d9ec67c09ccba003 Mon Sep 17 00:00:00 2001 From: Robin Braumann Date: Sun, 26 Apr 2026 20:42:04 +0200 Subject: [PATCH] fix(agents): guard interactive-summary against non-object outputs Without an object check, a malformed wire payload (string/number/null inside an unknown) would throw at the `'skipped' in resume` line. Bail on non-object outputs and return undefined so the caller falls back to just the tool name. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../agents/__tests__/interactive-summary.test.ts | 9 +++++++++ .../src/features/agents/utils/interactive-summary.ts | 9 ++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/frontend/editor-ui/src/features/agents/__tests__/interactive-summary.test.ts b/packages/frontend/editor-ui/src/features/agents/__tests__/interactive-summary.test.ts index 95a523be1bd..0faed56b81c 100644 --- a/packages/frontend/editor-ui/src/features/agents/__tests__/interactive-summary.test.ts +++ b/packages/frontend/editor-ui/src/features/agents/__tests__/interactive-summary.test.ts @@ -15,6 +15,15 @@ describe('summariseInteractiveOutput', () => { expect(summariseInteractiveOutput(ASK_QUESTION_TOOL_NAME, undefined)).toBeUndefined(); }); + it.each([null, 'oops', 42, true, ['x']])( + 'returns undefined for non-object output (%p)', + (value) => { + expect(summariseInteractiveOutput(ASK_CREDENTIAL_TOOL_NAME, value)).toBeUndefined(); + expect(summariseInteractiveOutput(ASK_LLM_TOOL_NAME, value)).toBeUndefined(); + expect(summariseInteractiveOutput(ASK_QUESTION_TOOL_NAME, value)).toBeUndefined(); + }, + ); + it('joins ask_question option labels when input + output present', () => { const input = { question: 'Where to post?', diff --git a/packages/frontend/editor-ui/src/features/agents/utils/interactive-summary.ts b/packages/frontend/editor-ui/src/features/agents/utils/interactive-summary.ts index 3f520ea2e06..83b283ec694 100644 --- a/packages/frontend/editor-ui/src/features/agents/utils/interactive-summary.ts +++ b/packages/frontend/editor-ui/src/features/agents/utils/interactive-summary.ts @@ -17,12 +17,19 @@ import { * Returns `undefined` for non-interactive tools or when the output isn't * shaped as expected — callers fall back to rendering just the tool name. */ +function isPlainObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + export function summariseInteractiveOutput( toolName: string, output: unknown, input?: unknown, ): string | undefined { - if (output === undefined || output === null) return undefined; + // Output comes off the wire as `unknown`; treat anything non-object-shaped + // as malformed and bail. This prevents `in` / property access from + // throwing when a malformed payload sneaks through. + if (!isPlainObject(output)) return undefined; if (toolName === ASK_QUESTION_TOOL_NAME) { const resume = output as AskQuestionResume;