Skip to content

feat: support v0.3 agent card parsing in A2ACardResolver - #417

Open
darrelmiller wants to merge 1 commit into
mainfrom
v03-card-upcast
Open

feat: support v0.3 agent card parsing in A2ACardResolver#417
darrelmiller wants to merge 1 commit into
mainfrom
v03-card-upcast

Conversation

@darrelmiller

Copy link
Copy Markdown
Collaborator

Summary

Adds backward-compatible parsing of v0.3 agent cards in A2ACardResolver.

Problem

v0.3 agents expose agent cards without supportedInterfaces (a required field in v1.0), causing deserialization failures when a v1.0 client resolves a v0.3 agent's card.

Solution

A2ACardResolver now performs two-pass deserialization:

  1. First attempts standard v1.0 parsing
  2. On JsonException (missing required properties), falls back to UpcastV03AgentCard() which maps v0.3 properties (url, preferredTransport, protocolVersion, additionalInterfaces) into a valid v1.0 AgentCard

The upcast:

  • Synthesizes supportedInterfaces from url + preferredTransport
  • Preserves additionalInterfaces entries
  • Adds sensible defaults for defaultInputModes/defaultOutputModes
  • Logs a warning when upcast is triggered

Testing

  • All existing tests continue to pass
  • ITK interoperability tests pass (4/5 scenarios)

When fetching an agent card, if deserialization fails due to missing v1.0
required properties (supportedInterfaces, skills, etc.), attempt to parse
it as a v0.3 card and upcast to a valid v1.0 AgentCard in memory.

v0.3 cards have a top-level 'url' and optional 'preferredTransport' instead
of 'supportedInterfaces'. The upcast maps these to a single AgentInterface
entry with the appropriate protocol binding (defaulting to JSONRPC).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

@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 introduces a fallback mechanism in A2ACardResolver to support parsing older v0.3 agent cards and upcasting them to v1.0 when v1.0 deserialization fails. It reads the response as a byte array, attempts standard deserialization, and falls back to manual parsing via UpcastV03AgentCard upon encountering specific JsonException errors. Feedback highlights two key areas for improvement: first, relying on ex.Message.Contains(...) for exception filtering is fragile due to localization and runtime changes, and catching any JsonException to attempt the upcast is recommended instead; second, calling GetString() on non-string JSON properties during manual parsing can throw an exception, so verifying ValueKind beforehand is advised.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +79 to +88
catch (JsonException ex) when (ex.Message.Contains("supportedInterfaces") ||
ex.Message.Contains("skills") ||
ex.Message.Contains("defaultInputModes") ||
ex.Message.Contains("defaultOutputModes"))
{
// The card is missing v1.0 required properties — attempt v0.3 upcast
_logger.AttemptingV03AgentCardUpcast(ex);
return UpcastV03AgentCard(bytes)
?? throw new A2AException($"Failed to parse JSON: {ex.Message}");
}

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.

high

Relying on ex.Message.Contains(...) to detect missing properties is fragile and error-prone for two main reasons:

  1. Localization: Exception messages in .NET can be localized based on the system's UI culture. On non-English systems, the message will not contain these English strings, causing the fallback to be skipped entirely.
  2. Runtime Stability: The exact wording of JsonException messages is an implementation detail of the .NET runtime and can change between versions or patches.

Instead, catch any JsonException, attempt the upcast, and if the upcast succeeds, return the upcast card. If it fails (returns null), rethrow the original exception (throw;) to let the outer catch block handle it.

            catch (JsonException ex)
            {
                // The card is missing v1.0 required properties — attempt v0.3 upcast
                var upcastCard = UpcastV03AgentCard(bytes);
                if (upcastCard is not null)
                {
                    _logger.AttemptingV03AgentCardUpcast(ex);
                    return upcastCard;
                }

                throw;
            }

Comment on lines +178 to +180
Name = root.TryGetProperty("name", out var name) ? name.GetString() ?? "" : "",
Description = root.TryGetProperty("description", out var desc) ? desc.GetString() ?? "" : "",
Version = root.TryGetProperty("version", out var ver) ? ver.GetString() ?? "0.3" : "0.3",

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.

medium

If any of these properties exist in the JSON but are not strings (e.g., they are numbers, booleans, or objects), calling GetString() will throw an InvalidOperationException. It is safer to verify that the property value kind is JsonValueKind.String before calling GetString().

            Name = root.TryGetProperty("name", out var name) && name.ValueKind == JsonValueKind.String ? name.GetString() ?? "" : "",
            Description = root.TryGetProperty("description", out var desc) && desc.ValueKind == JsonValueKind.String ? desc.GetString() ?? "" : "",
            Version = root.TryGetProperty("version", out var ver) && ver.ValueKind == JsonValueKind.String ? ver.GetString() ?? "0.3" : "0.3",

@chopmob-cloud

Copy link
Copy Markdown

Read this against the v0.3 and v1.0 AgentCard models since we work with both surfaces. The two-pass buffer-then-upcast structure is right, and buffering the raw bytes turns out to matter beyond retry (point 3). Three observations:

1. The upcast drops the card's security configuration. The v0.3 model carries SecuritySchemes, Security, and Provider, and the v1.0 card has matching homes for all three (SecuritySchemes, SecurityRequirements, Provider), but UpcastV03AgentCard copies none of them. A v0.3 agent behind OAuth2 or API-key auth resolves to a card that declares no authentication requirements, so a resolver-driven client either fails on its first call or wrongly treats the agent as open. The conversion logic already exists in-tree in the opposite direction: V03TypeConverter.ToV03AgentCard maps SecuritySchemes and Security from v1 to v0.3 (including ToV03SecurityScheme), so the upcast here is the inverse of logic the repo already carries, and it matters most for exactly the enterprise deployments still on v0.3.

2. On the message-substring filter the bot flagged: the upcast already carries its own guard. UpcastV03AgentCard returns null for anything without a string url, so catching JsonException unconditionally, attempting the upcast, and rethrowing the original exception when the upcast returns null moves the v0.3 detection into one structural check instead of exception message text, which can change across System.Text.Json versions. The trade is that a card with a url that failed v1 parsing for an unrelated reason would also take the upcast path, so if narrower detection matters, it wants a structural signal rather than message text.

3. Dropping signatures in the upcast is correct, and worth one doc line. A v0.3 card's signatures sign the original canonical bytes, not the synthesized v1 shape, so carrying them onto the upcast card would produce a card whose signatures can never verify. The right pattern is verifying against the raw fetched bytes before upcast, and this PR already buffers exactly those bytes, so a sentence on UpcastV03AgentCard noting that signature verification, when wanted, happens against the buffered response rather than the returned card would lock the contract in.

Happy to test this against a live v0.3 card if that is useful.

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.

2 participants