Repository navigation
Description
- I have searched the existing issues to make sure this bug has not already been reported.
Describe the Bug
A validator that takes the v0.9.1 schemas from a2ui.org and retrieves their references by URL rejects valid v0.9.1 messages. .github/workflows/docs.yml publishes each version's json/ files at the version root, so https://a2ui.org/specification/v0_9_1/server_to_client_list.json serves v0.9.1's list schema. That file declares the $id https://a2ui.org/specification/v0_9/server_to_client_list.json. Its "$ref": "server_to_client.json" resolves against that $id to https://a2ui.org/specification/v0_9/server_to_client.json, which serves v0.9's schema, whose version is "const": "v0.9". So every "v0.9.1" message in the list fails. client_to_server_list.json reaches v0.9's client_to_server.json the same way, and the wrapper files and sample.json refer to the lists the same way. Ajv and Python's jsonschema ('v0.9' was expected) both reject the messages.
There are two gaps.
1. The v0.9.1 schemas declare v0.9's $ids. Every schema in specification/v0_9_1/json/ declares the $id of its v0.9 counterpart. When #1493 created v0.9.1 as a copy of v0.9, the two json/ folders were identical, so each $id named one document. #1561 then changed the version field of v0.9.1's server_to_client.json, client_to_server.json and client_data_model.json from "const": "v0.9" to "enum": ["v0.9", "v0.9.1"], and kept the $ids. So https://a2ui.org/specification/v0_9/server_to_client.json now names two different documents, and so does .../client_data_model.json.
A tool that registers the schemas by $id instead of fetching them cannot hold both versions either. Ajv refuses v0.9.1's copy (schema with key or id "https://a2ui.org/specification/v0_9/server_to_client.json" already exists), and a lookup by the $id of v0.9.1's list returns v0.9's list.
2. client_to_server.json declares no $id, in v0.9 or in v0.9.1. Every other schema in both folders declares one, and v1.0's renderer_to_agent.json declares https://a2ui.org/specification/v1_0/renderer_to_agent.json. client_to_server_list.json refers to it as "client_to_server.json" (line 8), which resolves to https://a2ui.org/specification/v0_9/client_to_server.json. A fetching validator gets v0.9's file there, as above. A tool that registers schemas by $id finds no schema at that address, so client_to_server_list.json does not compile in either version (can't resolve reference client_to_server.json from id https://a2ui.org/specification/v0_9/client_to_server_list.json). conformance/tests/test_conformance_yaml.py adds the $id to v0.9's file by hand before registering it (V09_CLIENT_TO_SERVER), and the Swift schema generator falls back to the same address. Nothing in the repo compiles client_to_server_list.json, so CI does not see the gap.
Steps to Reproduce
After yarn install, save this as repro.mjs at the repo root and run node repro.mjs:
import {createRequire} from 'node:module';
const require = createRequire(import.meta.url);
const Ajv2020 = require('ajv/dist/2020').default;
const addFormats = require('ajv-formats').default;
const SITE = 'https://a2ui.org/specification/';
async function get(url) {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status} for ${url}`);
return response.json();
}
// a2ui.org does not serve the "catalog.json" placeholder that the schemas
// refer to, so the validator supplies the basic catalog there. It fetches
// every other referenced schema by URL.
const catalog = await get(`${SITE}v0_9_1/catalogs/basic/catalog.json`);
async function check(url, message) {
const ajv = new Ajv2020({
strict: false,
allErrors: true,
loadSchema: async uri => {
if (uri.endsWith('/catalog.json')) return {...catalog, $id: uri};
console.log(` fetches ${uri}`);
return get(uri);
},
});
addFormats(ajv);
const schema = await get(url);
console.log(`${url}\n declares $id ${schema.$id}`);
const validate = await ajv.compileAsync(schema);
const valid = validate(message);
console.log(` ${JSON.stringify(message)}: ${valid ? 'valid' : 'invalid'}`);
for (const e of new Set((validate.errors ?? []).filter(e => e.instancePath.endsWith('/version')).map(e => `${e.instancePath} ${e.message} ${JSON.stringify(e.params)}`))) {
console.log(` ${e}`);
}
}
const action = {name: 'submit', surfaceId: 's', sourceComponentId: 'b', timestamp: '2026-11-04T00:00:00Z', context: {}};
await check(`${SITE}v0_9_1/server_to_client_list.json`, [{version: 'v0.9.1', deleteSurface: {surfaceId: 's'}}]);
await check(`${SITE}v0_9_1/client_to_server_list.json`, [{version: 'v0.9.1', action}]);
// Counter-cases: v0.9.1's server_to_client.json itself, and v0.9's list.
await check(`${SITE}v0_9_1/server_to_client.json`, {version: 'v0.9.1', deleteSurface: {surfaceId: 's'}});
await check(`${SITE}v0_9/server_to_client_list.json`, [{version: 'v0.9', deleteSurface: {surfaceId: 's'}}]);Output:
https://a2ui.org/specification/v0_9_1/server_to_client_list.json
declares $id https://a2ui.org/specification/v0_9/server_to_client_list.json
fetches https://a2ui.org/specification/v0_9/server_to_client.json
fetches https://a2ui.org/specification/v0_9/common_types.json
[{"version":"v0.9.1","deleteSurface":{"surfaceId":"s"}}]: invalid
/0/version must be equal to constant {"allowedValue":"v0.9"}
https://a2ui.org/specification/v0_9_1/client_to_server_list.json
declares $id https://a2ui.org/specification/v0_9/client_to_server_list.json
fetches https://a2ui.org/specification/v0_9/client_to_server.json
[{"version":"v0.9.1","action":{"name":"submit","surfaceId":"s","sourceComponentId":"b","timestamp":"2026-11-04T00:00:00Z","context":{}}}]: invalid
/0/version must be equal to constant {"allowedValue":"v0.9"}
https://a2ui.org/specification/v0_9_1/server_to_client.json
declares $id https://a2ui.org/specification/v0_9/server_to_client.json
fetches https://a2ui.org/specification/v0_9/common_types.json
{"version":"v0.9.1","deleteSurface":{"surfaceId":"s"}}: valid
https://a2ui.org/specification/v0_9/server_to_client_list.json
declares $id https://a2ui.org/specification/v0_9/server_to_client_list.json
fetches https://a2ui.org/specification/v0_9/server_to_client.json
fetches https://a2ui.org/specification/v0_9/common_types.json
[{"version":"v0.9","deleteSurface":{"surfaceId":"s"}}]: valid
v0.9.1's server_to_client.json accepts the v0.9.1 message on its own, and v0.9's list accepts a v0.9 message. v0.9.1's lists reject v0.9.1 messages because they resolve to v0.9's schemas.
Expected Behavior
- A v0.9.1 message validates against the published v0.9.1 schemas, whether a validator fetches their references from a2ui.org or registers the schemas by
$id. For that, the v0.9.1 schemas declare the addresses they are published at,https://a2ui.org/specification/v0_9_1/<name>.json. client_to_server.jsondeclareshttps://a2ui.org/specification/v0_9/client_to_server.jsonin v0.9 andhttps://a2ui.org/specification/v0_9_1/client_to_server.jsonin v0.9.1, soclient_to_server_list.jsonalso compiles in a registry.
Screenshots / Video / Logs
n/a
Environment Details
- OS: macOS
- Browser/Platform: Node.js 22.15.0; Python 3.14.3 for the
jsonschemacheck - SDK/Package Name & Version: the v0.9 and v0.9.1 schemas served by a2ui.org on 2026-11-04 and
specification/atmain@43696ef43; ajv 8.18.0, ajv-formats 3.0.1; jsonschema 4.26.0 - Protocol Version: v0.9 and v0.9.1
- Agent Framework & LLM Model: n/a, reproduced without a model
Additional Context
Suggested fix:
- Give every schema in
specification/v0_9_1/json/exceptcommon_types.jsonthe$idhttps://a2ui.org/specification/v0_9_1/<name>.json, and give bothclient_to_server.jsonfiles an$id. - Keep
common_types.jsonand the basic catalog under v0.9's$ids. Both are byte-identical to v0.9's. The catalog's$idis also itscatalogId: the v0.9 and v0.9.1a2ui_protocol.mdsay both fields should be set to the same URI. The v0.9.1 extension specification giveshttps://a2ui.org/specification/v0_9/catalogs/basic/catalog.jsonas the basic catalog's id. The catalog refers tocommon_types.jsonby its absolute v0.9 address, 78 times. web_core'sDEFAULT_CATALOG_COMPATIBILITYtreats a v0.9 catalog and a v0.9.1 catalog as compatible both ways. Could a maintainer confirm that sharing the catalog is intended? The alternative, giving v0.9.1 its own basic catalog identity, would change thecatalogIdthat v0.9.1 agents and renderers exchange. - Follow the change where code depends on these
$ids. a2ui-core hard-codes v0.9's$idfor its generated v0.9.1server_to_clientschema (_AGENT_TO_RENDERER_SPECS). The conformance casetest_v091_agent_to_renderer_schema_matches_specificationcompares that schema with the specification file exactly.specification/v0_9_1/test/run_tests.pybuilds itscatalog.jsonalias from the catalog's$id, with a regex that does not matchv0_9_1/. After 1 and 2,server_to_client.jsonresolvescatalog.jsonunderv0_9_1/andcommon_types.jsonunderv0_9/, so the runner needs an alias at each address.
Related:
- fix(web_core): restore listStyle to v0.9 basic catalog ListApi schema #3956 (open, approved): adds
List.listStyleto v0.9's basic catalog only. After it merges, v0.9's and v0.9.1's catalogs differ while sharing one$idandcatalogId. - refactor(spec): Standardize on absolute URIs for schema and catalog references #2030 (open): proposes absolute URIs for schema and catalog references, so that offline validator registries resolve them without mapping short names.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsIn Progress
Activity
Varun-S10 commentedon Nov 4, 2026
Hi @retz8, Thank you for raising the issue. We have reproduced the issue and we appreciate you have raised pr to fix the issue. The pr will be reviewed. Thank you for your patience and understanding.