From 2c44acc2eb225695325f3eb9a662f27bba000d06 Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Mon, 26 Oct 2026 18:40:10 -0700 Subject: [PATCH 1/3] - --- blueprints/modules/a2ui_core.blueprint.md | 7 +-- conformance/core/rpc_functions.yaml | 22 +++++++ specification/v1_0/docs/a2ui_protocol.md | 2 +- .../RPCConformanceTests.swift | 62 ++++++++++--------- 4 files changed, 59 insertions(+), 34 deletions(-) diff --git a/blueprints/modules/a2ui_core.blueprint.md b/blueprints/modules/a2ui_core.blueprint.md index 66f0bb5af7..eee05730a8 100644 --- a/blueprints/modules/a2ui_core.blueprint.md +++ b/blueprints/modules/a2ui_core.blueprint.md @@ -1000,13 +1000,12 @@ export class RpcHandler { #### RPC Execution Lifecycle & Safeguards 1. **Inbound Call Execution (`handleCallRendererFunction`)**: - - **Catalog Resolution Order**: An explicit `catalogId` and its absence are two separate paths, not steps in one fallback chain. - 1. If the call carries a `catalogId`, resolve it against the surface's `availableCatalogs`, then against the processor's global catalog list. If neither has it, return `RpcErrorCode.INVALID_FUNCTION_CALL`. Do not fall through to the surface default: the caller named a catalog, and answering with a different one is worse than failing. - 2. If the call carries no `catalogId`, use the surface's `defaultCatalog`. - 3. If neither path yields a catalog, return `RpcErrorCode.INVALID_FUNCTION_CALL`. + - **Catalog Resolution Order**: Resolve the call's `catalogId` against the surface's `availableCatalogs`, then against the processor's global catalog list. If neither has it, return `RpcErrorCode.INVALID_FUNCTION_CALL`. Do not fall through to the surface default: the caller named a catalog, and answering with a different one is worse than failing. Falling back to the first entry of the global catalog list is wrong for the same reason. It resolves against a catalog the surface may never have negotiated, so a typo in `catalogId` executes a same-named function from an unrelated catalog instead of failing. Once resolved, verify the catalog's `protocolVersion` is compatible with the surface before executing. + A call with no `catalogId` never reaches this step. The schema requires `catalogId` on `callRendererFunction`, so inbound parsing rejects the message with `A2uiValidationError`, as it does a message with no `functionCallId`, and no `rendererFunctionResponse` is sent. Do not use the surface's `defaultCatalog` for such a call, and do not answer it with `INVALID_FUNCTION_CALL`: an agent can check this field against the schema before it sends the message, so a renderer should never see it. + - **Function Resolution & Argument Coercion**: Look up the function in the resolved catalog. If it is absent, or argument validation fails, return `RpcErrorCode.INVALID_FUNCTION_CALL`. Execute with the coerced arguments returned by `validateFunction`, never the raw payload. - **Permission & Activation Boundaries**: Verifies execution permissions. If a function requires user activation (e.g., audio playback or clipboard write), execution checks that `isUserActivated === true`. - **Headless Fallback Context**: If called without an active DOM view context, provisions a headless `DataContext` backed by the surface or root data model so dynamic value resolutions and data path updates resolve cleanly without null reference errors. diff --git a/conformance/core/rpc_functions.yaml b/conformance/core/rpc_functions.yaml index cd5cec1aa9..9559f67aff 100644 --- a/conformance/core/rpc_functions.yaml +++ b/conformance/core/rpc_functions.yaml @@ -268,6 +268,28 @@ error: message: "callFunction" +- name: test_rpc_call_renderer_function_invalid_payload_missing_catalog_id + description: Verifies a callRendererFunction whose callFunction has no catalogId fails schema validation instead of being answered with INVALID_FUNCTION_CALL, even when a held catalog defines the function. + catalog: + protocolVersion: "v1.0" + action: handle_rpc + args: + message: + version: "v1.0" + callRendererFunction: + functionCallId: "call-110" + callFunction: + "@call": "playMedia" + args: + mediaId: "video-789" + functionMetadata: + playMedia: + allowedCallers: "rendererOrAgent" + requiresUserActivation: false + expect: + error: + message: "catalogId" + - name: test_rpc_agent_function_response_inbound description: Verifies inbound agentFunctionResponse message processing by MessageProcessor. catalog: diff --git a/specification/v1_0/docs/a2ui_protocol.md b/specification/v1_0/docs/a2ui_protocol.md index 4b0787b25b..1ab986e95c 100644 --- a/specification/v1_0/docs/a2ui_protocol.md +++ b/specification/v1_0/docs/a2ui_protocol.md @@ -306,7 +306,7 @@ This message is sent by the agent to execute a function registered on the render - `functionCallId` (string, required): A unique identifier for this invocation instance. The renderer MUST copy this ID verbatim into the subsequent `rendererFunctionResponse` message. - `callFunction` (object, required): The description of the function call. - `@call` (string, required): The registered name of the function to execute. - - `catalogId` (string, required): The catalog ID defining the function to execute. `catalogId` MUST be specified on `callRendererFunction`; if omitted or unregistered, the renderer rejects the call with `code: "INVALID_FUNCTION_CALL"`. + - `catalogId` (string, required): The catalog ID defining the function to execute. `catalogId` MUST be specified on `callRendererFunction`. A call without it violates the schema, so the message is malformed, the same as one without `functionCallId`, and the renderer rejects it as invalid instead of answering it. Agents should validate outgoing messages against the schema so that a missing `catalogId` never reaches the renderer. If `catalogId` names a catalog the renderer has not registered, the renderer rejects the call with `code: "INVALID_FUNCTION_CALL"`. - `args` (object, optional): Arguments passed to the function, as defined by its schema in the catalog. **Evaluation Scope and Ordering:** diff --git a/swift/core/Tests/A2UIConformanceTests/RPCConformanceTests.swift b/swift/core/Tests/A2UIConformanceTests/RPCConformanceTests.swift index b0e7599975..217cf61e99 100644 --- a/swift/core/Tests/A2UIConformanceTests/RPCConformanceTests.swift +++ b/swift/core/Tests/A2UIConformanceTests/RPCConformanceTests.swift @@ -133,40 +133,44 @@ struct RPCConformanceTests { // Inbound call if let messageJSON = testCase.args?["message"] { - let data = try JSONEncoder().encode(messageJSON) + let expectedErr = testCase.expect?["error"]?.objectValue + let message: AgentToRendererMessage? do { - let message = try JSONDecoder().decode(AgentToRendererMessage.self, from: data) - if case .callRendererFunction(let callMsg) = message { - let response = await rpcHandler.handleIncomingCall( - callMsg, - catalogs: catalogs, - userActivationPresent: userActivation - ) - - if let expectedResp = testCase.expect?["response"]?["rendererFunctionResponse"] { - let expectedCallID = expectedResp["functionCallId"]?.stringValue - #expect(response.functionCallID == expectedCallID) - - if let expectedValue = expectedResp["value"] { - #expect(response.value == expectedValue) - } + // parseAll runs the version adapter's schema checks, which a bare + // JSONDecoder skips (e.g. a missing callFunction.catalogId). + message = try AgentToRendererMessage.parseAll(messageJSON).first + #expect(expectedErr == nil, "\(testCase.name): expected a validation error") + } catch { + // If schema validation rejected invalid payload (e.g. missing callFunction) + #expect(expectedErr != nil, "\(testCase.name): unexpected error \(error)") + let msgSub = expectedErr?["message"]?.stringValue ?? "" + #expect("\(error)".contains(msgSub)) + message = nil + } + if case .callRendererFunction(let callMsg)? = message { + let response = await rpcHandler.handleIncomingCall( + callMsg, + catalogs: catalogs, + userActivationPresent: userActivation + ) + + if let expectedResp = testCase.expect?["response"]?["rendererFunctionResponse"] { + let expectedCallID = expectedResp["functionCallId"]?.stringValue + #expect(response.functionCallID == expectedCallID) - if let expectedErr = expectedResp["error"] { - let expectedCode = expectedErr["code"]?.stringValue - let expectedMsg = expectedErr["message"]?.stringValue - #expect(response.error?.code == expectedCode) - if let expectedMsg { - #expect(response.error?.message.contains(expectedMsg) == true) - } + if let expectedValue = expectedResp["value"] { + #expect(response.value == expectedValue) + } + + if let expectedErr = expectedResp["error"] { + let expectedCode = expectedErr["code"]?.stringValue + let expectedMsg = expectedErr["message"]?.stringValue + #expect(response.error?.code == expectedCode) + if let expectedMsg { + #expect(response.error?.message.contains(expectedMsg) == true) } } } - } catch { - // If schema validation rejected invalid payload (e.g. missing callFunction) - if let expectedErr = testCase.expect?["error"]?.objectValue { - let msgSub = expectedErr["message"]?.stringValue ?? "" - #expect("\(error)".contains(msgSub)) - } } } From ab6830f5cc609ed40d1299fd955419824316d1a1 Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Mon, 26 Oct 2026 18:46:13 -0700 Subject: [PATCH 2/3] Update RPCConformanceTests.swift --- .../Tests/A2UIConformanceTests/RPCConformanceTests.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/swift/core/Tests/A2UIConformanceTests/RPCConformanceTests.swift b/swift/core/Tests/A2UIConformanceTests/RPCConformanceTests.swift index 217cf61e99..facf79ea92 100644 --- a/swift/core/Tests/A2UIConformanceTests/RPCConformanceTests.swift +++ b/swift/core/Tests/A2UIConformanceTests/RPCConformanceTests.swift @@ -143,8 +143,9 @@ struct RPCConformanceTests { } catch { // If schema validation rejected invalid payload (e.g. missing callFunction) #expect(expectedErr != nil, "\(testCase.name): unexpected error \(error)") - let msgSub = expectedErr?["message"]?.stringValue ?? "" - #expect("\(error)".contains(msgSub)) + if let msgSub = expectedErr?["message"]?.stringValue { + #expect("\(error)".contains(msgSub)) + } message = nil } if case .callRendererFunction(let callMsg)? = message { From 7eb4b48b3ab43cad31c423565b7ff27a5ffc9ec7 Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Tue, 27 Oct 2026 10:04:35 -0700 Subject: [PATCH 3/3] - --- blueprints/modules/a2ui_core.blueprint.md | 6 +++--- conformance/core/rpc_functions.yaml | 2 +- specification/v1_0/docs/a2ui_protocol.md | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/blueprints/modules/a2ui_core.blueprint.md b/blueprints/modules/a2ui_core.blueprint.md index eee05730a8..9d5268bcb4 100644 --- a/blueprints/modules/a2ui_core.blueprint.md +++ b/blueprints/modules/a2ui_core.blueprint.md @@ -1000,11 +1000,11 @@ export class RpcHandler { #### RPC Execution Lifecycle & Safeguards 1. **Inbound Call Execution (`handleCallRendererFunction`)**: - - **Catalog Resolution Order**: Resolve the call's `catalogId` against the surface's `availableCatalogs`, then against the processor's global catalog list. If neither has it, return `RpcErrorCode.INVALID_FUNCTION_CALL`. Do not fall through to the surface default: the caller named a catalog, and answering with a different one is worse than failing. + - **Catalog Resolution Order**: Resolve the call's `catalogId` against the surface's `availableCatalogs`, then against the processor's global catalog list. If neither has it, return `RpcErrorCode.INVALID_FUNCTION_CALL`. Do not fall through to the surface default. - Falling back to the first entry of the global catalog list is wrong for the same reason. It resolves against a catalog the surface may never have negotiated, so a typo in `catalogId` executes a same-named function from an unrelated catalog instead of failing. Once resolved, verify the catalog's `protocolVersion` is compatible with the surface before executing. + Falling back to the first entry of the global catalog list is also wrong. It resolves against a catalog the surface may never have negotiated, so a typo in `catalogId` executes a same-named function from an unrelated catalog instead of failing. Once resolved, verify the catalog's `protocolVersion` is compatible with the surface before executing. - A call with no `catalogId` never reaches this step. The schema requires `catalogId` on `callRendererFunction`, so inbound parsing rejects the message with `A2uiValidationError`, as it does a message with no `functionCallId`, and no `rendererFunctionResponse` is sent. Do not use the surface's `defaultCatalog` for such a call, and do not answer it with `INVALID_FUNCTION_CALL`: an agent can check this field against the schema before it sends the message, so a renderer should never see it. + A call with no `catalogId` should not reach this step. The schema requires `catalogId` on `callRendererFunction`, so inbound parsing should reject the message with `A2uiValidationError` and no `rendererFunctionResponse` will be sent. Agents should check this field against the schema before sending the message. - **Function Resolution & Argument Coercion**: Look up the function in the resolved catalog. If it is absent, or argument validation fails, return `RpcErrorCode.INVALID_FUNCTION_CALL`. Execute with the coerced arguments returned by `validateFunction`, never the raw payload. - **Permission & Activation Boundaries**: Verifies execution permissions. If a function requires user activation (e.g., audio playback or clipboard write), execution checks that `isUserActivated === true`. diff --git a/conformance/core/rpc_functions.yaml b/conformance/core/rpc_functions.yaml index 9559f67aff..4f0365c547 100644 --- a/conformance/core/rpc_functions.yaml +++ b/conformance/core/rpc_functions.yaml @@ -269,7 +269,7 @@ message: "callFunction" - name: test_rpc_call_renderer_function_invalid_payload_missing_catalog_id - description: Verifies a callRendererFunction whose callFunction has no catalogId fails schema validation instead of being answered with INVALID_FUNCTION_CALL, even when a held catalog defines the function. + description: Verifies a callRendererFunction whose callFunction has no catalogId fails schema validation, even when a held catalog defines the function. catalog: protocolVersion: "v1.0" action: handle_rpc diff --git a/specification/v1_0/docs/a2ui_protocol.md b/specification/v1_0/docs/a2ui_protocol.md index 1ab986e95c..23d46d1582 100644 --- a/specification/v1_0/docs/a2ui_protocol.md +++ b/specification/v1_0/docs/a2ui_protocol.md @@ -306,7 +306,7 @@ This message is sent by the agent to execute a function registered on the render - `functionCallId` (string, required): A unique identifier for this invocation instance. The renderer MUST copy this ID verbatim into the subsequent `rendererFunctionResponse` message. - `callFunction` (object, required): The description of the function call. - `@call` (string, required): The registered name of the function to execute. - - `catalogId` (string, required): The catalog ID defining the function to execute. `catalogId` MUST be specified on `callRendererFunction`. A call without it violates the schema, so the message is malformed, the same as one without `functionCallId`, and the renderer rejects it as invalid instead of answering it. Agents should validate outgoing messages against the schema so that a missing `catalogId` never reaches the renderer. If `catalogId` names a catalog the renderer has not registered, the renderer rejects the call with `code: "INVALID_FUNCTION_CALL"`. + - `catalogId` (string, required): The catalog ID defining the function to execute. `catalogId` MUST be specified on `callRendererFunction`. A call without it violates the schema, so the message is malformed, and should be rejected. Agents should validate outgoing messages against the schema so that a missing `catalogId` never reaches the renderer. If `catalogId` names a catalog the renderer has not registered, the renderer rejects the call with `code: "INVALID_FUNCTION_CALL"`. - `args` (object, optional): Arguments passed to the function, as defined by its schema in the catalog. **Evaluation Scope and Ordering:**