Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions .design/a2a-grpc-transport.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# A2A Bridge: gRPC Transport

**Status:** Implementing
**Created:** 2026-06-05
**Related:** [a2a-bridge-design.md](./a2a-bridge-design.md), [a2a-multi-turn-lifecycle.md](./a2a-multi-turn-lifecycle.md)

---

## 1. Problem

The A2A bridge currently only supports JSON-RPC 2.0 over HTTP. The A2A protocol
specification defines three transport bindings: JSON-RPC, gRPC, and HTTP+JSON/REST.
The ITK compatibility tests exercise gRPC transport in their multi-hop traversal.
Without gRPC, the bridge cannot participate in cross-SDK interop scenarios that
use gRPC transport.

## 2. Design

### Approach: gRPC adapter over existing Bridge logic

The Bridge struct already implements all A2A operations (SendMessage, GetTask,
ListTasks, CancelTask, push notifications, streaming). The gRPC server is a thin
adapter that:

1. Receives gRPC requests
2. Translates protobuf messages to the bridge's internal types
3. Delegates to the same Bridge methods the JSON-RPC server uses
4. Translates responses back to protobuf

This avoids duplicating any business logic.

### Proto compilation

Use the official A2A proto from `a2aproject/A2A/specification/a2a.proto` as the
source. Compile with `protoc` + `protoc-gen-go` + `protoc-gen-go-grpc` to generate
Go code. Generated code goes in `internal/a2apb/`.

The proto has dependencies on `google/api/annotations.proto` etc. — use buf or
vendored google API protos.

### Server structure

```
extras/scion-a2a-bridge/
internal/
a2apb/ # generated protobuf Go code
bridge/
grpc_server.go # gRPC adapter implementing A2AServiceServer
```

### gRPC service methods → Bridge method mapping

| gRPC RPC | Bridge method |
|---|---|
| SendMessage | Bridge.SendMessage (blocking or non-blocking based on return_immediately) |
| SendStreamingMessage | Bridge.SendStreamingMessage |
| GetTask | Bridge.GetTask |
| ListTasks | Bridge.ListTasks |
| CancelTask | Bridge.CancelTask |
| SubscribeToTask | Bridge.SubscribeToTask |
| CreateTaskPushNotificationConfig | Bridge.SetPushNotificationConfig |
| GetTaskPushNotificationConfig | Bridge.GetPushNotificationConfig |
| ListTaskPushNotificationConfigs | Bridge.GetPushNotificationConfig |
| DeleteTaskPushNotificationConfig | Bridge.DeletePushNotificationConfig |
| GetExtendedAgentCard | Bridge.GenerateAgentCard |

### Configuration

Add to config:
```yaml
bridge:
grpc_listen_address: ":9443" # separate port for gRPC
```

### Startup

The main() function starts both HTTP and gRPC servers. gRPC is optional — only
started if grpc_listen_address is configured.

## 3. Scope

- In: gRPC server adapter, proto compilation, config, startup wiring
- Out: gRPC client (for making outbound A2A calls), TLS on gRPC (use reverse proxy)

## 4. Testing

- Test: each gRPC RPC maps correctly to Bridge method
- Test: protobuf ↔ internal type translation
- Test: streaming RPCs deliver events correctly
- Test: gRPC server starts and accepts connections
30 changes: 29 additions & 1 deletion extras/scion-a2a-bridge/cmd/scion-a2a-bridge/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"flag"
"fmt"
"log/slog"
"net"
"net/http"
"os"
"os/signal"
Expand All @@ -32,6 +33,7 @@ import (
secretmanager "cloud.google.com/go/secretmanager/apiv1"
smpb "cloud.google.com/go/secretmanager/apiv1/secretmanagerpb"
"github.com/prometheus/client_golang/prometheus"
"google.golang.org/grpc"
"gopkg.in/yaml.v3"

"github.com/GoogleCloudPlatform/scion/extras/scion-a2a-bridge/internal/bridge"
Expand Down Expand Up @@ -154,7 +156,7 @@ func main() {
MaxHeaderBytes: 1 << 20,
}

errCh := make(chan error, 1)
errCh := make(chan error, 2)
go func() {
log.Warn("A2A server starting WITHOUT TLS — ensure TLS is terminated at a reverse proxy (e.g. Caddy, nginx, cloud LB)", "address", listenAddr)
log.Info("A2A protocol server starting", "address", listenAddr)
Expand All @@ -163,6 +165,27 @@ func main() {
}
}()

// Start gRPC server if configured.
var grpcServer *grpc.Server
if cfg.Bridge.GRPCListenAddress != "" {
grpcServer = grpc.NewServer()
grpcSrv := bridge.NewGRPCServer(b, cfg, log.With("component", "grpc-server"))
grpcSrv.Register(grpcServer)

grpcLis, err := net.Listen("tcp", cfg.Bridge.GRPCListenAddress)
if err != nil {
log.Error("failed to listen for gRPC", "address", cfg.Bridge.GRPCListenAddress, "error", err)
os.Exit(1)
}

go func() {
log.Info("gRPC server starting", "address", cfg.Bridge.GRPCListenAddress)
if err := grpcServer.Serve(grpcLis); err != nil {
errCh <- fmt.Errorf("grpc server: %w", err)
}
}()
}

log.Info("scion-a2a-bridge ready")

// Wait for shutdown signal.
Expand All @@ -183,6 +206,11 @@ func main() {
log.Error("failed to stop A2A server", "error", err)
}

if grpcServer != nil {
grpcServer.GracefulStop()
log.Info("gRPC server stopped")
}

// Drain background goroutines before closing the store.
b.Shutdown()

Expand Down
12 changes: 6 additions & 6 deletions extras/scion-a2a-bridge/go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/GoogleCloudPlatform/scion/extras/scion-a2a-bridge

go 1.25.4
go 1.26.1

require (
cloud.google.com/go/secretmanager v1.16.0
Expand All @@ -9,6 +9,11 @@ require (
github.com/google/uuid v1.6.0
github.com/hashicorp/go-plugin v1.7.0
github.com/mattn/go-sqlite3 v1.14.28
github.com/prometheus/client_golang v1.23.2
github.com/prometheus/client_model v0.6.2
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9
google.golang.org/grpc v1.80.0
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
)

Expand All @@ -33,8 +38,6 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/oklog/run v1.1.0 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.2 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
Expand All @@ -54,10 +57,7 @@ require (
golang.org/x/time v0.14.0 // indirect
google.golang.org/api v0.259.0 // indirect
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/grpc v1.80.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
)

replace github.com/GoogleCloudPlatform/scion => ../../
6 changes: 6 additions & 0 deletions extras/scion-a2a-bridge/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,14 @@ github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94=
github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
Expand Down Expand Up @@ -112,6 +116,8 @@ go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfC
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
Expand Down
Loading