From c16794e2203d6d7d0fb63e76b0416475021d0161 Mon Sep 17 00:00:00 2001 From: Sam Betts <1769706+Tehsmash@users.noreply.github.com> Date: Thu, 27 Aug 2026 15:56:30 +0100 Subject: [PATCH] feat: add SLIMRPC transport support Add --transport slimrpc backed by github.com/agntcy/slim-a2a-go, the Go reference implementation of the experimental SLIMRPC custom protocol binding (https://github.com/a2aproject/experimental-cpb-slimrpc). The implementation is CGO-gated: slim.go (cgo build tag) wires up the SLIM runtime; slim_nocgo.go returns a clear error when CGO is disabled. New flags (all used only with --transport slimrpc): --slim-endpoint SLIM node endpoint --slim-local-name local SLIM identity (org/ns/app) --slim-identity-provider-type identity provider type (sharedSecret) --slim-identity-provider-shared-secret shared secret for provider --slim-identity-verifier-type identity verifier type (sharedSecret) --slim-identity-verifier-shared-secret shared secret for verifier CI: install the SLIM native library via slim-bindings-setup before build/test, cache ~/.cgo-cache across runs, and set CGO_ENABLED=1. Closes #17 Signed-off-by: Sam Betts <1769706+Tehsmash@users.noreply.github.com> --- .github/actions/setup/action.yml | 5 ++ .github/workflows/go.yaml | 4 ++ go.mod | 6 ++- go.sum | 2 + internal/cli/client.go | 32 +++++++++--- internal/cli/root.go | 14 ++++++ internal/cli/slim.go | 85 ++++++++++++++++++++++++++++++++ internal/cli/slim_nocgo.go | 27 ++++++++++ internal/flagparse/transports.go | 6 ++- 9 files changed, 172 insertions(+), 9 deletions(-) create mode 100644 internal/cli/slim.go create mode 100644 internal/cli/slim_nocgo.go diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index df4994c..bf4096b 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -22,5 +22,10 @@ runs: path: | ~/.cache/go-build ~/go/pkg/mod + ~/.cgo-cache key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.mod', '**/go.sum') }} restore-keys: ${{ runner.os }}-go-build- + + - name: Install SLIM bindings native library + shell: bash + run: go run github.com/agntcy/slim-bindings-go/v2/cmd/slim-bindings-setup diff --git a/.github/workflows/go.yaml b/.github/workflows/go.yaml index 0fc9ac2..7976d78 100644 --- a/.github/workflows/go.yaml +++ b/.github/workflows/go.yaml @@ -32,9 +32,13 @@ jobs: - name: Build run: go build -mod=readonly -v ./... + env: + CGO_ENABLED: "1" - name: Test run: go test -race -mod=readonly -v -count=1 -shuffle=on ./... + env: + CGO_ENABLED: "1" lint: runs-on: ubuntu-latest diff --git a/go.mod b/go.mod index c80efb1..d7502ac 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/a2aproject/a2a-cli -go 1.25.0 +go 1.25.2 require ( github.com/a2aproject/a2a-go v0.3.15 @@ -13,6 +13,8 @@ require ( ) require ( + github.com/agntcy/slim-a2a-go v0.3.0 + github.com/agntcy/slim-bindings-go/v2 v2.1.1 github.com/google/uuid v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect golang.org/x/mod v0.35.0 // indirect @@ -24,3 +26,5 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 // indirect google.golang.org/protobuf v1.36.11 // indirect ) + +replace github.com/agntcy/slim-a2a-go => /Users/sambetts/Work/src/github.com/agntcy/slim-a2a-go diff --git a/go.sum b/go.sum index 8ccaabc..c7114dd 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/a2aproject/a2a-go v0.3.15 h1:h5YpCiPq3jxQ5rIns7oDjPag3ivP8u817AzdA4F+ github.com/a2aproject/a2a-go v0.3.15/go.mod h1:I7Cm+a1oL+UT6zMoP+roaRE5vdfUa1iQGVN8aSOuZ0I= github.com/a2aproject/a2a-go/v2 v2.4.1-0.20260817112309-579cff89f7ba h1:4EedjlTSsl92mWP9qAfa8zKHI7mLzOkMpFiSHl9Y8u0= github.com/a2aproject/a2a-go/v2 v2.4.1-0.20260817112309-579cff89f7ba/go.mod h1:EghJ/rY9OCC6jme1z+otOBf+1YCVVCpMSPZU31t3hKY= +github.com/agntcy/slim-bindings-go/v2 v2.1.1 h1:I2Ac5GRH7W+1gp3Vxm2me0wME0TVroPuK1uvkivp9t0= +github.com/agntcy/slim-bindings-go/v2 v2.1.1/go.mod h1:LfBa89/DfmYRg9EeU9DxkyXhJTBjsJ0OLIpxA1iBuhc= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= diff --git a/internal/cli/client.go b/internal/cli/client.go index 41df7b4..1e80cd1 100644 --- a/internal/cli/client.go +++ b/internal/cli/client.go @@ -66,8 +66,13 @@ func newClientFromEndpoint(ctx context.Context, cfg *globalConfig, ref string, e } cfg.logf("connecting directly to %s via %s (skipping card resolution)", endpointURL, protocol) + factoryOpts, err := clientFactoryOpts(cfg) + if err != nil { + return nil, err + } + endpoint := a2a.NewAgentInterface(endpointURL, protocol) - client, err := a2aclient.NewFromEndpoints(ctx, []*a2a.AgentInterface{endpoint}, append(clientFactoryOpts(cfg), extraOpts...)...) + client, err := a2aclient.NewFromEndpoints(ctx, []*a2a.AgentInterface{endpoint}, append(factoryOpts, extraOpts...)...) return client, hintInsecure(err) } @@ -90,7 +95,11 @@ func newClientFromCard(ctx context.Context, cfg *globalConfig, ref string, extra return nil, fmt.Errorf("resolving agent card: %w", err) } - factoryOpts := append(clientFactoryOpts(cfg), extraOpts...) + factoryOpts, err := clientFactoryOpts(cfg) + if err != nil { + return nil, err + } + factoryOpts = append(factoryOpts, extraOpts...) if len(protos) > 0 { factoryOpts = append(factoryOpts, a2aclient.WithConfig(a2aclient.Config{PreferredTransports: protos})) } @@ -107,8 +116,8 @@ func hintInsecure(err error) error { return err } -func clientFactoryOpts(cfg *globalConfig) []a2aclient.FactoryOption { - factoryOpts := []a2aclient.FactoryOption{ +func clientFactoryOpts(cfg *globalConfig) ([]a2aclient.FactoryOption, error) { + opts := []a2aclient.FactoryOption{ a2av0.WithRESTTransport(a2av0.RESTTransportConfig{}), a2av0.WithJSONRPCTransport(a2av0.JSONRPCTransportConfig{}), } @@ -116,13 +125,24 @@ func clientFactoryOpts(cfg *globalConfig) []a2aclient.FactoryOption { if cfg.insecureGRPC { grpcOpts = append(grpcOpts, grpc.WithTransportCredentials(insecure.NewCredentials())) } - factoryOpts = append(factoryOpts, + opts = append(opts, a2agrpcv0.WithGRPCTransport(grpcOpts...), a2agrpc.WithGRPCTransport(grpcOpts...), ) - return factoryOpts + for _, t := range cfg.transports { + if strings.EqualFold(t, "slimrpc") { + slimOpt, err := withSLIMRPCTransport(cfg) + if err != nil { + return nil, err + } + opts = append(opts, slimOpt) + break + } + } + return opts, nil } + func stripHTTPScheme(raw string) string { u, err := url.Parse(raw) if err != nil || (u.Scheme != "http" && u.Scheme != "https") { diff --git a/internal/cli/root.go b/internal/cli/root.go index 5f74d56..f50280f 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -56,6 +56,14 @@ type globalConfig struct { insecureGRPC bool configPath string + // SLIM RPC transport options (only used when --transport slimrpc) + slimEndpoint string + slimLocalName string + slimIdentityProviderType string + slimIdentityProviderSharedSecret string + slimIdentityVerifierType string + slimIdentityVerifierSharedSecret string + bindings []clicfg.FlagBinding *output.Printer @@ -122,6 +130,12 @@ func newRootCmd(cfg *globalConfig, deps deps) *cobra.Command { pf.BoolVarP(&cfg.verbose, "verbose", "v", false, "Verbose output to stderr") pf.BoolVar(&cfg.insecureGRPC, "insecure", false, "Use insecure (plaintext) gRPC transport credentials") pf.StringVar(&cfg.configPath, "config", "", "Load configuration from an explicit .env file in place of the local .env") + pf.StringVar(&cfg.slimEndpoint, "slim-endpoint", "http://127.0.0.1:46357", "SLIM node endpoint (used with --transport slimrpc)") + pf.StringVar(&cfg.slimLocalName, "slim-local-name", "agntcy/a2a-cli/client", "Local SLIM identity name org/namespace/app (used with --transport slimrpc)") + pf.StringVar(&cfg.slimIdentityProviderType, "slim-identity-provider-type", "", "SLIM identity provider type: sharedSecret (required with --transport slimrpc)") + pf.StringVar(&cfg.slimIdentityProviderSharedSecret, "slim-identity-provider-shared-secret", "", "Shared secret for the SLIM identity provider (used when --slim-identity-provider-type=sharedSecret)") + pf.StringVar(&cfg.slimIdentityVerifierType, "slim-identity-verifier-type", "", "SLIM identity verifier type: sharedSecret (required with --transport slimrpc)") + pf.StringVar(&cfg.slimIdentityVerifierSharedSecret, "slim-identity-verifier-shared-secret", "", "Shared secret for the SLIM identity verifier (used when --slim-identity-verifier-type=sharedSecret)") cmd.AddCommand( newCardCmd(cfg), diff --git a/internal/cli/slim.go b/internal/cli/slim.go new file mode 100644 index 0000000..021686d --- /dev/null +++ b/internal/cli/slim.go @@ -0,0 +1,85 @@ +//go:build cgo + +// Copyright 2026 The A2A Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cli + +import ( + "fmt" + + "github.com/a2aproject/a2a-go/v2/a2aclient" + a2aslimrpc "github.com/agntcy/slim-a2a-go/a2aslimrpc/v1" + slim_bindings "github.com/agntcy/slim-bindings-go/v2" +) + +func withSLIMRPCTransport(cfg *globalConfig) (a2aclient.FactoryOption, error) { + localName, err := slim_bindings.NameFromString(cfg.slimLocalName) + if err != nil { + return nil, fmt.Errorf("--slim-local-name: %w", err) + } + + identityProvider, err := slimIdentityProvider(cfg) + if err != nil { + return nil, err + } + + identityVerifier, err := slimIdentityVerifier(cfg) + if err != nil { + return nil, err + } + + slim_bindings.InitializeWithDefaults() + svc := slim_bindings.GetGlobalService() + + app, err := svc.CreateApp(localName, identityProvider, identityVerifier) + if err != nil { + return nil, fmt.Errorf("slim: create app: %w", err) + } + + connID, err := svc.Connect(slim_bindings.NewInsecureClientConfig(cfg.slimEndpoint)) + if err != nil { + return nil, fmt.Errorf("slim: connect to %s: %w", cfg.slimEndpoint, err) + } + + if err := app.Subscribe(localName, &connID); err != nil { + return nil, fmt.Errorf("slim: subscribe: %w", err) + } + + cfg.logf("connected to SLIM node at %s as %s", cfg.slimEndpoint, cfg.slimLocalName) + + return a2aslimrpc.WithSLIMRPCTransport(app, &connID), nil +} + +func slimIdentityProvider(cfg *globalConfig) (slim_bindings.IdentityProviderConfig, error) { + switch cfg.slimIdentityProviderType { + case "sharedSecret": + return slim_bindings.IdentityProviderConfigSharedSecret{Data: cfg.slimIdentityProviderSharedSecret}, nil + case "": + return nil, fmt.Errorf("--slim-identity-provider-type is required with --transport slimrpc (use sharedSecret)") + default: + return nil, fmt.Errorf("--slim-identity-provider-type: unknown type %q (use sharedSecret)", cfg.slimIdentityProviderType) + } +} + +func slimIdentityVerifier(cfg *globalConfig) (slim_bindings.IdentityVerifierConfig, error) { + switch cfg.slimIdentityVerifierType { + case "sharedSecret": + return slim_bindings.IdentityVerifierConfigSharedSecret{Data: cfg.slimIdentityVerifierSharedSecret}, nil + case "": + return nil, fmt.Errorf("--slim-identity-verifier-type is required with --transport slimrpc (use sharedSecret)") + default: + return nil, fmt.Errorf("--slim-identity-verifier-type: unknown type %q (use sharedSecret)", cfg.slimIdentityVerifierType) + } +} diff --git a/internal/cli/slim_nocgo.go b/internal/cli/slim_nocgo.go new file mode 100644 index 0000000..29bf8c3 --- /dev/null +++ b/internal/cli/slim_nocgo.go @@ -0,0 +1,27 @@ +//go:build !cgo + +// Copyright 2026 The A2A Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cli + +import ( + "fmt" + + "github.com/a2aproject/a2a-go/v2/a2aclient" +) + +func withSLIMRPCTransport(_ *globalConfig) (a2aclient.FactoryOption, error) { + return nil, fmt.Errorf("slimrpc transport requires CGO; rebuild without CGO_ENABLED=0") +} diff --git a/internal/flagparse/transports.go b/internal/flagparse/transports.go index c4c940b..32638cf 100644 --- a/internal/flagparse/transports.go +++ b/internal/flagparse/transports.go @@ -46,7 +46,7 @@ func SingleTransport(ss []string) (a2a.TransportProtocol, error) { return "", err } if len(protos) != 1 { - return "", fmt.Errorf("exactly one --transport is required (rest, jsonrpc, or grpc)") + return "", fmt.Errorf("exactly one --transport is required (rest, jsonrpc, grpc, or slimrpc)") } return protos[0], nil } @@ -59,7 +59,9 @@ func parseTransport(s string) (a2a.TransportProtocol, error) { return a2a.TransportProtocolJSONRPC, nil case "grpc": return a2a.TransportProtocolGRPC, nil + case "slimrpc": + return a2a.TransportProtocol("slimrpc"), nil default: - return "", fmt.Errorf("unknown transport %q (use rest, jsonrpc, or grpc)", s) + return "", fmt.Errorf("unknown transport %q (use rest, jsonrpc, grpc, or slimrpc)", s) } }