feat: Add A2A parser plugin for inbound request inspection#363
Conversation
Parses A2A JSON-RPC 2.0 request bodies (message/send, message/stream) and populates pctx.Extensions.A2A with session, message, role, and parts for downstream policy plugins (e.g., guardrails). - Expand A2AExtension with RPCID, SessionID, MessageID, Role fields - Rename A2APart.Type to Kind to match A2A protocol field name - Register a2a-parser in the plugin registry - Add comprehensive debug logging for LOG_LEVEL=debug troubleshooting Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
- Make message extraction generic: extract from params.message regardless of method name (forward-compatible with future A2A methods) - Add uri fallback for file parts (A2A spec allows uri as alternative to data) - Consolidate debug logging: one summary log instead of per-field calls - Rename test to AnyMethod, add FutureMethodWithMessage test - Add DataPart test (exercises json.Marshal path) - Add FilePartURI test (exercises uri fallback) Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
| case "file": | ||
| content, _ = partMap["data"].(string) | ||
| if content == "" { | ||
| content, _ = partMap["uri"].(string) | ||
| } |
There was a problem hiding this comment.
Ref https://a2a-protocol.org/latest/definitions/#protobuf I'm curious how this case maps. Does another part of Kagenti form file data?
There was a problem hiding this comment.
Good question. Looking at the A2A spec, the current spec (June 2025 draft) uses a unified Part with oneof content: text, raw (base64 bytes), url (file pointer), or data (structured JSON), plus optional mediaType/filename metadata.
The older SDK samples (which Kagenti UI currently uses) still send {"kind": "file", "data": "..."} or {"kind": "file", "uri": "..."} — that is what this code handles today. No part of Kagenti currently creates file parts; this branch just ensures we do not lose content if a future A2A client sends one.
As the spec stabilizes, we should update this to match the canonical Part structure (no kind discriminator, use mediaType + content field presence instead). I will add a TODO comment noting this.
Dead code in this PR -- no callers remain after removing the method switch. The A2A parser (PR rossoctl#363) will re-add them when it merges. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
Malformed parts (non-string kind, empty kind) are now silently skipped instead of producing empty A2APart entries. Hardens the parser against non-compliant or malicious payloads. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
Add test for type-mismatched part content (text: false, data: 0,
uri: {}). Skip nil data values in the data-part branch to avoid
serializing JSON null as content. Add TODO noting spec evolution.
Signed-off-by: Hai Huang <hai@us.ibm.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
mrsabath
left a comment
There was a problem hiding this comment.
Clean A2A parser plugin — mirrors MCP parser pattern, comprehensive tests (14 cases), all prior review feedback addressed. LGTM.
| content, _ = partMap["uri"].(string) | ||
| } | ||
| case "data": | ||
| if dataVal, ok := partMap["data"]; ok && dataVal != nil { |
There was a problem hiding this comment.
nit: This logs at Info level on every successfully parsed request. Since the parser runs on all inbound traffic (not just A2A), this could be noisy in production. Consider slog.Debug here and keeping only the existing Debug log below for detailed fields. The MCP parser uses slog.Info too, so this is consistent — but both might want to be Debug in the long run.
Dead code in this PR -- no callers remain after removing the method switch. The A2A parser (PR rossoctl#363) will re-add them when it merges. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
- Make message extraction generic: extract from params.message regardless of method name (forward-compatible with future A2A methods) - Add uri fallback for file parts (A2A spec allows uri as alternative to data) - Consolidate debug logging: one summary log instead of per-field calls - Rename test to AnyMethod, add FutureMethodWithMessage test - Add DataPart test (exercises json.Marshal path) - Add FilePartURI test (exercises uri fallback) Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
Summary
a2a-parserpipeline plugin that parses inbound A2A JSON-RPC 2.0 requests (message/send,message/stream) and populatespctx.Extensions.A2Afor downstream policy plugins (e.g., guardrails)A2AExtensiontype withRPCID,SessionID,MessageID,Rolefields to match the actual A2A protocol payload from Kagenti UIA2APart.TypetoKindto align with A2A protocol field naminga2a-parserin the plugin registry (opt-in via config, not added to defaults)Design
Follows the exact same pattern as
mcp-parser(PR #361):BodyAccess: trueandWrites: ["a2a"]Continue(never rejects -- parsing only)jsonRPCRequesthelper from the same packagePipeline placement
Debug logging
Comprehensive
slog.Debugcalls at every parsing step for early debugging.Enable with
LOG_LEVEL=debuginauthbridge-configConfigMap.Test plan
go vet ./...passesgo test ./...passes (all packages including existing MCP/JWT/token-exchange tests)go build ./...passes for cmd/authbridgea2a-parserto inbound pipeline config in Kind, send chat from UI, verifya2a-parser: parsed message/streamin logsAssisted-By: Claude (Anthropic AI) noreply@anthropic.com