Skip to content

fix(web_core): reject callRendererFunction without catalogId - #3555

Closed
innkuika wants to merge 4 commits into
a2ui-project:mainfrom
innkuika:fix/web-core-rpc-missing-catalog-id
Closed

innkuika wants to merge 4 commits into
a2ui-project:mainfrom
innkuika:fix/web-core-rpc-missing-catalog-id

Conversation

@innkuika

@innkuika innkuika commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

When an agent sent callRendererFunction without a catalogId, web_core threw a validation error and never replied, so the agent waited until it timed out. It now replies with an INVALID_FUNCTION_CALL response, as the v1.0 spec says. Fixes #3529.

Question for reviewers: web_core, Python and Swift all threw here before this change, and two of them had tests asserting it. The spec says to reply with INVALID_FUNCTION_CALL, so this PR follows the spec. If a missing catalogId should instead count as a malformed message, the spec text needs to change, and I'm happy to close this.

What changes

Details: root cause, fix, tests

Problem. The v1.0 spec (callRendererFunction, a2ui_protocol.md) says callFunction.catalogId is required, and that if it is omitted or unregistered, the renderer rejects the call with code: "INVALID_FUNCTION_CALL". It also says the renderer MUST always send a rendererFunctionResponse with the same functionCallId. RpcHandler already rejects both cases correctly. However, V1Point0Adapter first checks the whole envelope against the generated AgentToRendererMessageSchema union. When catalogId was omitted, processMessages and processMessagesAsync threw an A2uiValidationError that listed every branch of the union, and nothing reached outboundListener. An agent waiting for the response waited until it timed out. An unregistered catalogId was already handled correctly.

Fix.

  • BaseVersionAdapter gets a validatePayload hook. By default it does the same schema.safeParse as before.
  • V1Point0Adapter overrides it. When the union check fails and callFunction has no catalogId key, the adapter checks the message again with a schema derived from CallRendererFunctionMessageSchema. That schema keeps every other rule, including the strict envelope, functionCallId and FunctionCall, and drops only the catalogId requirement. If that check passes, the call goes to RpcHandler, which returns INVALID_FUNCTION_CALL ("No catalog available for function resolution."). The response carries the original functionCallId and also goes to outboundListener.
  • There is no fallback to the surface's default catalog, so this stays consistent with the spec and with how [v1.0][web_core] RpcHandler.resolveFunctionImplementation ignores surface.defaultCatalog when catalogId is omitted and defaults allowedCallers to rendererOnly #3515 was resolved. A catalogId that is present but not a string, or any other malformed callRendererFunction, still throws, as before. The existing conformance case test_rpc_call_renderer_function_invalid_payload_missing_call_function still passes.
  • The generated schema files and the public exports are unchanged. Agent-side schema validation still requires catalogId.

Tests.

  • New cases in message-processor.test.ts:
    • omitted catalogId through processMessagesAsync and through processMessages: no throw, and exactly one rendererFunctionResponse with the same functionCallId and INVALID_FUNCTION_CALL, both returned and sent to outboundListener
    • unregistered catalogId
    • a rejected call in a batch does not stop the rest of the batch
    • other malformed calls still throw and send nothing
  • The existing case that asserted the throw now asserts INVALID_FUNCTION_CALL, on a surface that has a default catalog, which also shows there is no fallback.
  • CHANGELOG entry added under Unreleased.

Not in this PR (parity). Python a2ui_core behaves the same way as web_core did before this fix: Pydantic requires catalogId, and tests/test_rpc.py asserts the A2uiValidationError. Swift has the same gap, tracked in #3530. Because of that, I did not add a shared conformance case. The TS runner would pass it, but the Python and Swift runners would fail until they are fixed.

Tests run (Node 22, typescript/core/web_core):

  • corepack yarn test: 1066/1066 pass. Conformance: 561 cases pass, 0 fail, including all 15 cases in core/rpc_functions.yaml.
  • corepack yarn format:check: clean. corepack yarn lint: 0 errors (warnings are the existing no-explicit-any ones).
  • Before the fix, the new tests failed with the same Invalid v1.0 message: createSurface: Expected object ... error shown in the issue.

Production build. The relaxed schema is built lazily on first use. Building it at module load broke the minified Angular production build (Cannot read properties of undefined (reading 'extend')). test:closure-compiler in typescript/renderers/angular/a2ui_explorer: 7/7 pass (7/7 fail without the lazy init).

Pre-launch Checklist

One time:

For this PR:

  • I have updated the relevant CHANGELOG.md file.
  • I updated/added relevant documentation.
  • My code changes (if any) have tests.
  • If my branch is on a fork, I have verified that scripts/e2e_test.sh passes.

If you need help, consider asking for advice on the discussion board.

…ALID_FUNCTION_CALL

The v1.0 specification requires the renderer to reject a
callRendererFunction whose callFunction.catalogId is omitted or
unregistered with a rendererFunctionResponse carrying the call's
functionCallId and code INVALID_FUNCTION_CALL. RpcHandler already does
this for both cases, but the v1.0 version adapter validated the whole
envelope against the generated union schema first, so an omitted
catalogId threw an A2uiValidationError listing every union branch from
processMessages/processMessagesAsync, and the agent never got a response.

The v1.0 adapter now accepts a callRendererFunction that fails the
union only because callFunction.catalogId is absent, validating the
rest of the message against the call schema without that requirement,
so the call reaches RpcHandler, which answers INVALID_FUNCTION_CALL.
There is no fallback to the surface's default catalog. Any other
malformed callRendererFunction still throws.

Fixes a2ui-project#3529

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the 'MessageProcessor' in specification v1.0 to handle 'callRendererFunction' messages that omit 'callFunction.catalogId' by returning a 'rendererFunctionResponse' with an 'INVALID_FUNCTION_CALL' error instead of throwing an 'A2uiValidationError'. To support this, a relaxed schema validation path was introduced in 'V1Point0Adapter'. The feedback suggests using 'Object.hasOwn' instead of the 'in' operator when checking for the presence of 'catalogId' to prevent potential prototype lookup issues.

Address review feedback: avoid the in operator's prototype lookup.
@innkuika innkuika changed the title fix(web_core): answer callRendererFunction without catalogId with INVALID_FUNCTION_CALL fix(web_core): reject callRendererFunction without catalogId Oct 23, 2026
Deriving it at module load read CallRendererFunctionMessageSchema before it
was initialized in the minified Angular production build, which failed with
"Cannot read properties of undefined (reading 'extend')" on every page.
Build it on first use instead.
@polina-c

Copy link
Copy Markdown
Collaborator

Thanks, the change is tightly scoped and the tests cover the cases. On your question for reviewers: I opened #3596 to settle it at the spec level before this merges.

In short, agent_to_renderer.json requires catalogId, so a call without it is a malformed message, while a2ui_protocol.md:309 says to answer it with INVALID_FUNCTION_CALL. All four cores follow the schema today. That includes Dart, which the description doesn't list: its oracle test requires the parser to agree with the JSON schema. So after this PR, web_core alone would answer. The core blueprint also says two things that conflict with this PR: RpcHandler step 2 falls back to the surface default, and any schema violation throws.

I lean toward the protocol doc, as this PR does. If #3596 goes that way, this PR should also update the blueprint and add a conformance case.

One small thing either way: the "malformed otherwise" test drops @call and adds an extra key in the same message, so it passes if either check works. Could you make those two separate cases?

🤖 Agent-generated

@innkuika

Copy link
Copy Markdown
Collaborator Author

Thanks Polina, I'll wait for #3596's conclusion before making further changes :)

@polina-c

Copy link
Copy Markdown
Collaborator

Fix: #3650

@github-project-automation github-project-automation Bot moved this from Todo to Done in A2UI Oct 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[v1.0][web_core] callRendererFunction without catalogId throws

2 participants