Skip to content

[BUG]: v0.9.1 schemas declare v0.9's $ids, and client_to_server.json declares none #3974

Open

Description

@retz8
  • 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.json declares https://a2ui.org/specification/v0_9/client_to_server.json in v0.9 and https://a2ui.org/specification/v0_9_1/client_to_server.json in v0.9.1, so client_to_server_list.json also 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 jsonschema check
  • SDK/Package Name & Version: the v0.9 and v0.9.1 schemas served by a2ui.org on 2026-11-04 and specification/ at main @ 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:

  1. Give every schema in specification/v0_9_1/json/ except common_types.json the $id https://a2ui.org/specification/v0_9_1/<name>.json, and give both client_to_server.json files an $id.
  2. Keep common_types.json and the basic catalog under v0.9's $ids. Both are byte-identical to v0.9's. The catalog's $id is also its catalogId: the v0.9 and v0.9.1 a2ui_protocol.md say both fields should be set to the same URI. The v0.9.1 extension specification gives https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json as the basic catalog's id. The catalog refers to common_types.json by its absolute v0.9 address, 78 times. web_core's DEFAULT_CATALOG_COMPATIBILITY treats 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 the catalogId that v0.9.1 agents and renderers exchange.
  3. Follow the change where code depends on these $ids. a2ui-core hard-codes v0.9's $id for its generated v0.9.1 server_to_client schema (_AGENT_TO_RENDERER_SPECS). The conformance case test_v091_agent_to_renderer_schema_matches_specification compares that schema with the specification file exactly. specification/v0_9_1/test/run_tests.py builds its catalog.json alias from the catalog's $id, with a regex that does not match v0_9_1/. After 1 and 2, server_to_client.json resolves catalog.json under v0_9_1/ and common_types.json under v0_9/, so the runner needs an alias at each address.

Related:

Activity

  1. added
    status: needs-triageauto-managed: https://github.com/a2ui-project/a2ui/blob/main/scripts/triage.mjs
    on Nov 4, 2026
  2. moved this from Todo to In Progress in A2UIon Nov 4, 2026
  3. added
    type: bugSomething isn't working
    P2The team intends to work on this in the near future, or at a lower priority.
    on Nov 4, 2026
  4. self-assigned this
    on Nov 4, 2026
  5. Varun-S10 commented on Nov 4, 2026

    @Varun-S10
    Collaborator

    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.

  6. added and removed
    status: needs-triageauto-managed: https://github.com/a2ui-project/a2ui/blob/main/scripts/triage.mjs
    on Nov 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

  • Status
    In Progress

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions