梦兽编程
AI_SUITE

Exposing: Expose Go Agent for External Calls

Detailed explanation of how to expose Go Agent to external systems via A2A protocol—service registration, endpoint exposure, auth configuration.

Exposing a Go Agent to external systems through the A2A protocol is the first step in building a distributed Agent network. But “exposing” is not the same as “open” — in production, exposing an Agent means dealing with authentication and authorization, access control, rate limiting and circuit breaking, service discovery, version management, and a whole range of other concerns.

This article will walk through how to expose Go Agents safely, reliably, and scalably—from basic configuration to production-grade hardening.


Basic Exposure: From Minimum Viable to Production-Ready

Minimal Exposure Configuration

package main

import (
    "context"
    "fmt"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"

    "google.golang.org/adk/a2a"
    "google.golang.org/adk/agent"
)

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    // Create the Agent
    weatherAgent, err := agent.New(agent.Config{
        Name:        "weather-agent",
        Model:       model,
        Instruction: "You are a weather query expert that provides accurate weather information.",
    })
    if err != nil {
        log.Fatalf("failed to create agent: %v", err)
    }

    // Create the A2A server
    server, err := a2a.NewServer(ctx,
        a2a.WithAgent(weatherAgent),
        a2a.WithPort(8080),
        a2a.WithAgentCard(a2a.AgentCard{
            Name:        "weather-agent",
            Version:     "1.0.0",
            Description: "Weather query expert supporting cities worldwide",
            URL:         "http://localhost:8080/a2a",
            Capabilities: a2a.Capabilities{
                Streaming:         true,
                PushNotifications: false,
            },
            Skills: []a2a.Skill{
                {
                    ID:          "current-weather",
                    Name:        "Current Weather Query",
                    Description: "Query the current weather conditions of a specified city",
                    InputModes:  []string{"text"},
                    OutputModes: []string{"text"},
                },
                {
                    ID:          "forecast",
                    Name:        "Weather Forecast",
                    Description: "Query the 7-day weather forecast",
                    InputModes:  []string{"text"},
                    OutputModes: []string{"text", "file"},
                },
            },
        }),
    )
    if err != nil {
        log.Fatalf("failed to create server: %v", err)
    }

    // Graceful shutdown handling
    sigCh := make(chan os.Signal, 1)
    signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)

    go func() {
        <-sigCh
        log.Println("shutting down server...")

        shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second)
        defer shutdownCancel()

        if err := server.Shutdown(shutdownCtx); err != nil {
            log.Printf("shutdown error: %v", err)
        }
        cancel()
    }()

    log.Println("A2A server starting on :8080")
    if err := server.Serve(); err != nil && err != http.ErrServerClosed {
        log.Fatalf("server error: %v", err)
    }
}

Exposed Endpoints

The A2A server automatically exposes the following standard endpoints:

EndpointMethodDescription
/.well-known/agent.jsonGETAgent Card (capability description)
/a2aPOSTMain entry point of the A2A protocol
/a2a/tasksPOSTCreate a task
/a2a/tasks/{id}GETGet task status
/a2a/tasks/{id}/cancelPOSTCancel a task
/healthGETHealth check

Service Registration: Making Agents Discoverable

Static Registration

In small systems, you can maintain the Agent registry manually:

// registry.go
type StaticRegistry struct {
    agents map[string]*a2a.AgentCard
    mu     sync.RWMutex
}

func NewStaticRegistry() *StaticRegistry {
    return &StaticRegistry{
        agents: make(map[string]*a2a.AgentCard),
    }
}

func (r *StaticRegistry) Register(card *a2a.AgentCard) error {
    r.mu.Lock()
    defer r.mu.Unlock()

    if _, exists := r.agents[card.Name]; exists {
        return fmt.Errorf("agent %s already registered", card.Name)
    }

    // Validate the Agent Card
    if err := validateAgentCard(card); err != nil {
        return fmt.Errorf("invalid agent card: %w", err)
    }

    r.agents[card.Name] = card
    log.Printf("agent registered: %s v%s at %s", card.Name, card.Version, card.URL)
    return nil
}

func (r *StaticRegistry) Discover(skill string) ([]*a2a.AgentCard, error) {
    r.mu.RLock()
    defer r.mu.RUnlock()

    var result []*a2a.AgentCard
    for _, card := range r.agents {
        for _, s := range card.Skills {
            if s.ID == skill || s.Name == skill {
                result = append(result, card)
                break
            }
        }
    }

    return result, nil
}

func (r *StaticRegistry) Get(name string) (*a2a.AgentCard, error) {
    r.mu.RLock()
    defer r.mu.RUnlock()

    card, exists := r.agents[name]
    if !exists {
        return nil, fmt.Errorf("agent %s not found", name)
    }

    return card, nil
}

func validateAgentCard(card *a2a.AgentCard) error {
    if card.Name == "" {
        return fmt.Errorf("name is required")
    }
    if card.URL == "" {
        return fmt.Errorf("URL is required")
    }
    if len(card.Skills) == 0 {
        return fmt.Errorf("at least one skill is required")
    }
    for i, skill := range card.Skills {
        if skill.ID == "" {
            return fmt.Errorf("skill[%d].id is required", i)
        }
        if skill.Name == "" {
            return fmt.Errorf("skill[%d].name is required", i)
        }
    }
    return nil
}

Dynamic Service Discovery (Consul)

In large distributed systems, use Consul or etcd for dynamic service discovery:

type ConsulRegistry struct {
    client *api.Client
    prefix string
}

func NewConsulRegistry(addr string) (*ConsulRegistry, error) {
    config := api.DefaultConfig()
    config.Address = addr

    client, err := api.NewClient(config)
    if err != nil {
        return nil, err
    }

    return &ConsulRegistry{
        client: client,
        prefix: "a2a/agents",
    }, nil
}

func (r *ConsulRegistry) Register(ctx context.Context, card *a2a.AgentCard) error {
    // Serialize the Agent Card
    cardData, err := json.Marshal(card)
    if err != nil {
        return err
    }

    // Register the service
    service := &api.AgentServiceRegistration{
        ID:      card.Name,
        Name:    "a2a-agent",
        Tags:    extractSkillTags(card.Skills),
        Port:    extractPort(card.URL),
        Address: extractHost(card.URL),
        Meta: map[string]string{
            "version":     card.Version,
            "agent_card":  string(cardData),
        },
        Check: &api.AgentServiceCheck{
            HTTP:     fmt.Sprintf("%s/health", card.URL),
            Interval: "10s",
            Timeout:  "5s",
            DeregisterCriticalServiceAfter: "1m",
        },
    }

    if err := r.client.Agent().ServiceRegister(service); err != nil {
        return fmt.Errorf("failed to register service: %w", err)
    }

    // Store the Agent Card in KV
    key := fmt.Sprintf("%s/%s", r.prefix, card.Name)
    _, err = r.client.KV().Put(&api.KVPair{
        Key:   key,
        Value: cardData,
    }, nil)

    return err
}

func (r *ConsulRegistry) Discover(ctx context.Context, skill string) ([]*a2a.AgentCard, error) {
    // Query healthy services
    services, _, err := r.client.Health().Service("a2a-agent", skill, true, nil)
    if err != nil {
        return nil, err
    }

    var agents []*a2a.AgentCard
    for _, svc := range services {
        cardData := svc.Service.Meta["agent_card"]
        if cardData == "" {
            continue
        }

        var card a2a.AgentCard
        if err := json.Unmarshal([]byte(cardData), &card); err != nil {
            log.Printf("failed to unmarshal agent card for %s: %v", svc.Service.ID, err)
            continue
        }

        agents = append(agents, &card)
    }

    return agents, nil
}

func (r *ConsulRegistry) Watch(ctx context.Context, callback func([]*a2a.AgentCard)) error {
    // Use Consul Watch for real-time updates
    plan, err := watch.Parse(map[string]interface{}{
        "type":    "service",
        "service": "a2a-agent",
    })
    if err != nil {
        return err
    }

    plan.Handler = func(idx uint64, raw interface{}) {
        if raw == nil {
            return
        }

        services, ok := raw.([]*api.ServiceEntry)
        if !ok {
            return
        }

        var agents []*a2a.AgentCard
        for _, svc := range services {
            cardData := svc.Service.Meta["agent_card"]
            if cardData == "" {
                continue
            }

            var card a2a.AgentCard
            if err := json.Unmarshal([]byte(cardData), &card); err != nil {
                continue
            }
            agents = append(agents, &card)
        }

        callback(agents)
    }

    return plan.RunWithClientAndLogger(r.client, nil)
}

Authentication Configuration: A Layered Defense System

API Key Authentication (Basics)

// Production-grade API Key authentication
func setupAPIKeyAuth() a2a.AuthMiddleware {
    // Load valid keys from environment variables or Secret Manager
    validKeys := loadValidAPIKeys()

    return a2a.APIKeyAuth{
        Header: "X-API-Key",
        Validator: func(key string) (string, error) {
            // 1. Check that the key exists
            clientID, exists := validKeys[key]
            if !exists {
                return "", fmt.Errorf("invalid API key")
            }

            // 2. Check that the key hasn't expired
            if isKeyExpired(key) {
                return "", fmt.Errorf("API key expired")
            }

            // 3. Record usage (for audit and rate limiting)
            recordKeyUsage(key)

            return clientID, nil
        },
    }
}

func loadValidAPIKeys() map[string]string {
    keys := make(map[string]string)

    // Load from environment variables (development)
    if envKeys := os.Getenv("A2A_API_KEYS"); envKeys != "" {
        for _, pair := range strings.Split(envKeys, ",") {
            parts := strings.SplitN(pair, ":", 2)
            if len(parts) == 2 {
                keys[parts[0]] = parts[1]
            }
        }
    }

    // Production: load from Secret Manager
    if metadata.OnGCE() {
        ctx := context.Background()
        client, err := secretmanager.NewClient(ctx)
        if err == nil {
            defer client.Close()

            req := &secretmanagerpb.AccessSecretVersionRequest{
                Name: "projects/my-project/secrets/a2a-api-keys/versions/latest",
            }
            result, err := client.AccessSecretVersion(ctx, req)
            if err == nil {
                var secretKeys map[string]string
                if err := json.Unmarshal(result.Payload.Data, &secretKeys); err == nil {
                    for k, v := range secretKeys {
                        keys[k] = v
                    }
                }
            }
        }
    }

    return keys
}

OAuth 2.0 / JWT Authentication (Enterprise)

import (
    "github.com/golang-jwt/jwt/v5"
)

type JWTAuth struct {
    publicKey *rsa.PublicKey
    issuer    string
    audience  string
}

func NewJWTAuth(publicKeyPEM []byte, issuer, audience string) (*JWTAuth, error) {
    block, _ := pem.Decode(publicKeyPEM)
    if block == nil {
        return nil, fmt.Errorf("failed to decode PEM block")
    }

    pub, err := x509.ParsePKIXPublicKey(block.Bytes)
    if err != nil {
        return nil, err
    }

    rsaPub, ok := pub.(*rsa.PublicKey)
    if !ok {
        return nil, fmt.Errorf("not an RSA public key")
    }

    return &JWTAuth{
        publicKey: rsaPub,
        issuer:    issuer,
        audience:  audience,
    }, nil
}

func (a *JWTAuth) Validate(tokenString string) (*jwt.MapClaims, error) {
    token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
        if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
            return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
        }
        return a.publicKey, nil
    },
        jwt.WithIssuer(a.issuer),
        jwt.WithAudience(a.audience),
        jwt.WithValidMethods([]string{"RS256"}),
    )
    if err != nil {
        return nil, fmt.Errorf("invalid token: %w", err)
    }

    if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
        return &claims, nil
    }

    return nil, fmt.Errorf("invalid claims")
}

// Use in the A2A server
func setupOAuthAuth() a2a.AuthMiddleware {
    publicKeyPEM := []byte(`-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----`)

    jwtAuth, err := NewJWTAuth(publicKeyPEM, "auth.example.com", "a2a-agent")
    if err != nil {
        log.Fatal(err)
    }

    return a2a.BearerAuth{
        Validator: func(token string) (string, error) {
            claims, err := jwtAuth.Validate(token)
            if err != nil {
                return "", err
            }

            // Extract the user/client ID
            sub, ok := (*claims)["sub"].(string)
            if !ok {
                return "", fmt.Errorf("missing sub claim")
            }

            // Check permissions
            scopes, ok := (*claims)["scope"].(string)
            if !ok || !strings.Contains(scopes, "a2a:invoke") {
                return "", fmt.Errorf("insufficient scope")
            }

            return sub, nil
        },
    }
}

mTLS (Mutual TLS) Authentication (Highest Security)

func setupMTLS() (*tls.Config, error) {
    // Load the server certificate
    cert, err := tls.LoadX509KeyPair("server.crt", "server.key")
    if err != nil {
        return nil, err
    }

    // Load the CA certificate (used to verify clients)
    caCert, err := os.ReadFile("ca.crt")
    if err != nil {
        return nil, err
    }

    caCertPool := x509.NewCertPool()
    caCertPool.AppendCertsFromPEM(caCert)

    return &tls.Config{
        Certificates: []tls.Certificate{cert},
        ClientCAs:    caCertPool,
        ClientAuth:   tls.RequireAndVerifyClientCert,
        MinVersion:   tls.VersionTLS12,
        CipherSuites: []uint16{
            tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
            tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
        },
    }, nil
}

// Use in the A2A server
server, err := a2a.NewServer(ctx,
    a2a.WithAgent(myAgent),
    a2a.WithPort(8443),
    a2a.WithTLS(setupMTLS()),
)

Rate Limiting and Protection

Layered Rate Limiting Strategy

// 1. Global rate limiting (protects the whole service)
func setupGlobalRateLimit() a2a.Middleware {
    limiter := rate.NewLimiter(rate.Limit(1000), 2000) // 1000/sec, burst 2000

    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            if !limiter.Allow() {
                http.Error(w, "service overloaded", http.StatusServiceUnavailable)
                return
            }
            next.ServeHTTP(w, r)
        })
    }
}

// 2. Per-client rate limiting (prevents a single client from exhausting resources)
type ClientRateLimiter struct {
    limiters sync.Map // map[string]*rate.Limiter
    rps      rate.Limit
    burst    int
}

func NewClientRateLimiter(rps rate.Limit, burst int) *ClientRateLimiter {
    return &ClientRateLimiter{
        rps:   rps,
        burst: burst,
    }
}

func (l *ClientRateLimiter) getLimiter(clientID string) *rate.Limiter {
    limiter, exists := l.limiters.Load(clientID)
    if exists {
        return limiter.(*rate.Limiter)
    }

    newLimiter := rate.NewLimiter(l.rps, l.burst)
    actual, loaded := l.limiters.LoadOrStore(clientID, newLimiter)
    if loaded {
        return actual.(*rate.Limiter)
    }
    return newLimiter
}

func (l *ClientRateLimiter) Middleware() a2a.Middleware {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            clientID := r.Context().Value("client_id").(string)
            if clientID == "" {
                http.Error(w, "unauthorized", http.StatusUnauthorized)
                return
            }

            limiter := l.getLimiter(clientID)
            if !limiter.Allow() {
                w.Header().Set("X-RateLimit-Limit", fmt.Sprintf("%v", l.rps))
                w.Header().Set("X-RateLimit-Remaining", "0")
                http.Error(w, "rate limit exceeded", http.StatusTooManyRequests)
                return
            }

            next.ServeHTTP(w, r)
        })
    }
}

// 3. Per-skill rate limiting (protects specific capabilities)
type SkillRateLimiter struct {
    limits map[string]rate.Limit
}

func (l *SkillRateLimiter) Middleware() a2a.Middleware {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            // Parse the skill from the request
            skill := r.Header.Get("X-A2A-Skill")
            if skill == "" {
                next.ServeHTTP(w, r)
                return
            }

            // Check the skill rate limit
            if limit, exists := l.limits[skill]; exists {
                // ... rate limiting logic
            }

            next.ServeHTTP(w, r)
        })
    }
}

Request Size Limiting

func setupRequestSizeLimit(maxSize int64) a2a.Middleware {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            r.Body = http.MaxBytesReader(w, r.Body, maxSize)
            next.ServeHTTP(w, r)
        })
    }
}

server, err := a2a.NewServer(ctx,
    a2a.WithAgent(myAgent),
    a2a.WithMiddleware(
        setupGlobalRateLimit(),
        NewClientRateLimiter(10, 20).Middleware(),  // 10 rps per client
        setupRequestSizeLimit(10*1024*1024),         // max 10MB
    ),
)

Version Management and Compatibility

Semantic Versioning

type Version struct {
    Major int
    Minor int
    Patch int
}

func (v Version) String() string {
    return fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch)
}

func (v Version) CompatibleWith(other Version) bool {
    // Same major version means compatible
    return v.Major == other.Major
}

// Version info in the Agent Card
type AgentCard struct {
    Name        string    `json:"name"`
    Version     string    `json:"version"`
    MinVersion  string    `json:"minVersion,omitempty"`  // minimum compatible version
    Deprecated  bool      `json:"deprecated,omitempty"`
    Deprecation string    `json:"deprecation,omitempty"` // deprecation notice
}

Running Multiple Versions Side by Side

// Expose multiple versions at the same time
func main() {
    ctx := context.Background()

    // v1 Agent
    agentV1, _ := agent.New(agent.Config{
        Name:    "weather-agent",
        Version: "1.0.0",
    })

    // v2 Agent (new features)
    agentV2, _ := agent.New(agent.Config{
        Name:    "weather-agent",
        Version: "2.0.0",
    })

    // v1 server
    serverV1, _ := a2a.NewServer(ctx,
        a2a.WithAgent(agentV1),
        a2a.WithPort(8081),
        a2a.WithPathPrefix("/v1"),
    )

    // v2 server
    serverV2, _ := a2a.NewServer(ctx,
        a2a.WithAgent(agentV2),
        a2a.WithPort(8082),
        a2a.WithPathPrefix("/v2"),
    )

    // Start both versions
    go serverV1.Serve()
    go serverV2.Serve()

    // Use Nginx or an API Gateway for version routing
}

Deep Dive: Common Questions

Q: Is it safe to expose an Agent to the public internet?

Absolutely not, unless you have the following protections in place:

  1. Transport layer: enforce HTTPS/TLS 1.2+
  2. Authentication layer: at least one of API Key / OAuth / mTLS
  3. Rate limiting layer: prevent DDoS and resource exhaustion
  4. Network layer: use VPC and firewall rules to restrict access sources
  5. Audit layer: log every call for traceability
// Production-grade security configuration
server, err := a2a.NewServer(ctx,
    a2a.WithAgent(myAgent),
    a2a.WithPort(443),
    a2a.WithTLS(tlsConfig),                          // TLS encryption
    a2a.WithAuth(setupOAuthAuth()),                  // OAuth authentication
    a2a.WithMiddleware(
        setupGlobalRateLimit(),                      // global rate limiting
        NewClientRateLimiter(10, 20).Middleware(),   // per-client rate limiting
        setupRequestSizeLimit(10*1024*1024),         // request size limit
        setupAuditLog(),                             // audit logging
    ),
    a2a.WithIPWhitelist([]string{"10.0.0.0/8"}),     // IP whitelist
)

Q: Can I limit the call rate?

You must. In production, configure at least three layers of rate limiting:

server, err := a2a.NewServer(ctx,
    a2a.WithRateLimit(100, time.Minute),  // global: 100 requests per minute
    a2a.WithClientRateLimit(10, time.Minute),  // per client: 10 requests per minute
    a2a.WithBurstLimit(20),  // burst request cap
)

Choosing a rate limiting strategy:

StrategyBest forImplementation complexity
Fixed windowSimple scenariosLow
Sliding windowSmooth rate limitingMedium
Token bucketAllows burst trafficMedium
Leaky bucketStrictly even pacingMedium

Next Steps

Exposing is done—next let’s look at how to consume external Agents.

A2A Protocol | Consuming →


Follow “Mengshou Programming” on WeChat for more hands-on Go ADK tutorials—weekly updates on practical Go / AI programming content.

Frequently Asked Questions

What is an Agent Card?

It is metadata describing the Agent's name, version, URL, capabilities, and skills, exposed at /.well-known/agent.json.

What is the minimal setup to expose a Go Agent?

Create a2a.NewServer with the Agent, port, and an a2a.AgentCard configuration.

What security concerns matter when exposing an Agent?

Authentication, authorization, rate limiting, circuit breaking, service discovery, and versioning.

What endpoints does the A2A server expose by default?

/.well-known/agent.json, /a2a, /a2a/tasks, and task status endpoints.

How do you gracefully shut down the A2A server?

Listen for SIGINT/SIGTERM, create a timeout context, and call server.Shutdown.