Skip to content

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

Open
darrelmiller wants to merge 3 commits into
mainfrom
v03-card-upcast
Open

feat: support v0.3 agent card parsing in A2ACardResolver#417
darrelmiller wants to merge 3 commits 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 thread src/A2A/Client/A2ACardResolver.cs Outdated
Comment thread src/A2A/Client/A2ACardResolver.cs Outdated
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
Contributor

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.

- Remove fragile exception message filtering; catch all JsonException
- Add root object guard to prevent InvalidOperationException on non-object JSON
- Require top-level protocolVersion as v0.3 discriminator
- Add ValueKind checks on all GetString() calls
- Update log message to reflect broader catch semantics

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

Copy link
Copy Markdown
Contributor

Verified the hardening commit independently against current main. The move off the ex.Message.Contains(...) filter to an unconditional catch (JsonException) that attempts the upcast and rethrows the original on null is the right shape, and the new protocolVersion discriminator makes it safe rather than merely broader.

Two things worth recording, since I checked them against the models directly:

The discriminator is sound. ProtocolVersion is [JsonRequired] on the v0.3 AgentCard model (default "0.3.0"), so a conformant v0.3 card always carries a top-level protocolVersion string. Gating the upcast on it therefore cannot reject a valid v0.3 card, and it does narrow the fallback so a v1.0 parse that failed for an unrelated reason no longer lands in the upcast path. The ValueKind == JsonValueKind.String guards added before each GetString() are also correct: GetString() throws on a non string token, so a card with, say, a numeric name would previously have escaped the catch as an InvalidOperationException.

One item from the earlier note this commit does not touch: the upcast still omits securitySchemes, security, and provider. The v0.3 AgentCard model carries all three, and the v1 card has homes for them (SecuritySchemes, SecurityRequirements, Provider), so a v0.3 agent behind OAuth2 or an API key resolves to a card that declares no auth. A resolver-driven client keying off SecurityRequirements then attaches no credentials or treats the agent as open, so the drop reads as a quiet fail-open on the card's protection metadata. provider and security map across fairly directly; securitySchemes needs the inverse of the existing V03TypeConverter.ToV03SecurityScheme, since the scheme types differ between the two surfaces.

Happy to run this against a live v0.3 card carrying security config if that is useful.

};

// Also include additionalInterfaces if present (a v0.3 extension)
if (root.TryGetProperty("additionalInterfaces", out var addlInterfaces) && addlInterfaces.ValueKind == JsonValueKind.Array)

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.

Nice direction on the v0.3 fallback. One spec-shape issue here: v0.3 additionalInterfaces entries are { "transport": "...", "url": "..." }, while the v1 AgentInterface type expects protocolBinding and protocolVersion.

So a valid v0.3 card with additional HTTP+JSON/GRPC interfaces can fail this fallback, or lose the transport binding. Could we parse this as the v0.3 shape explicitly and map:

transport → ProtocolBinding
url → Url
top-level protocolVersion → ProtocolVersion
A focused test with additionalInterfaces: [{ "transport": "HTTP+JSON", "url": "..." }] would catch this.

// Extract common fields
var card = new AgentCard
{
Name = root.TryGetProperty("name", out var name) && name.ValueKind == JsonValueKind.String ? name.GetString() ?? "" : "",

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.

several v0.3 AgentCard fields have v1 destinations but are not preserved here, especially provider, securitySchemes, security, supportsAuthenticatedExtendedCard, signatures, and skill-level security.

If the goal is only “resolve a usable endpoint from a v0.3 card,” this is fine but should be called out as scope. If the goal is “support v0.3 agent card parsing,” I think we should preserve those fields, especially the security metadata.

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.

3 participants