Repository: veraison/ratsd
Pull request #35: Add plugin library for ratsd
Author: login: cowbon
Created: 2025-02-28T16:56:59Z | Merged: 2025-03-05T17:44:41Z

Add the plugin framework similar to Veraison/service for sub-attesters. Unlike services, ratsd plugins communicates with ratsd core through GRPC,and there is only one type of the plugin. The verification for plugins will be added in the later patches.


Diff:
diff --git a/cmd/main.go b/cmd/main.go
index 33a4086..a0c8424 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -7,6 +7,7 @@ import (
 	"net/http"
 
 	"github.com/veraison/ratsd/api"
+	"github.com/veraison/ratsd/plugin"
 	"github.com/veraison/services/config"
 	"github.com/veraison/services/log"
 )
@@ -20,6 +21,7 @@ type cfg struct {
 	Protocol   string `mapstructure:"protocol" valid:"in(http|https)"`
 	Cert       string `mapstructure:"cert" config:"zerodefault"`
 	CertKey    string `mapstructure:"cert-key" config:"zerodefault"`
+	PluginDir  string `mapstructure:"plugin-dir" config:"zerodefault"`
 }
 
 func (o cfg) Validate() error {
@@ -60,6 +62,16 @@ func main() {
 		log.Fatalf("Could not load config: %v", err)
 	}
 
+	// Load sub-attesters from the path specified in config.yaml
+	pluginManager, err := plugin.CreateGoPluginManager(
+		cfg.PluginDir, log.Named("plugin"))
+
+	if err != nil {
+		log.Fatalf("could not create the plugin manager: %v", err)
+	}
+
+	log.Info("Loaded sub-attesters:", pluginManager.GetPluginList())
+
 	svr := api.NewServer(log.Named("api"))
 	r := http.NewServeMux()
 	h := api.HandlerFromMux(svr, r)
diff --git a/config.yaml b/config.yaml
index be4d112..c83fb84 100644
--- a/config.yaml
+++ b/config.yaml
@@ -3,3 +3,4 @@ logging:
 ratsd:
   listen-addr: 0.0.0.0:8895
   protocol: http
+  plugin-dir: attesters/bin
diff --git a/go.mod b/go.mod
index 8e0e3bb..4f64d97 100644
--- a/go.mod
+++ b/go.mod
@@ -4,6 +4,7 @@ go 1.22.7
 
 require (
 	github.com/getkin/kin-openapi v0.128.0
+	github.com/hashicorp/go-plugin v1.4.4
 	github.com/moogar0880/problems v0.1.1
 	github.com/oapi-codegen/runtime v1.1.1
 	github.com/stretchr/testify v1.9.0
@@ -32,9 +33,11 @@ require (
 	github.com/go-playground/universal-translator v0.18.1 // indirect
 	github.com/go-playground/validator/v10 v10.19.0 // indirect
 	github.com/goccy/go-json v0.10.2 // indirect
+	github.com/golang/protobuf v1.5.4 // indirect
 	github.com/google/uuid v1.6.0 // indirect
 	github.com/hashicorp/go-hclog v1.2.0 // indirect
 	github.com/hashicorp/hcl v1.0.0 // indirect
+	github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb // indirect
 	github.com/invopop/yaml v0.3.1 // indirect
 	github.com/josharian/intern v1.0.0 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
@@ -44,11 +47,13 @@ require (
 	github.com/mailru/easyjson v0.7.7 // indirect
 	github.com/mattn/go-colorable v0.1.13 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
+	github.com/mitchellh/go-testing-interface v1.0.0 // indirect
 	github.com/mitchellh/mapstructure v1.5.0 // indirect
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
 	github.com/oapi-codegen/oapi-codegen/v2 v2.4.1 // indirect
+	github.com/oklog/run v1.0.0 // indirect
 	github.com/pelletier/go-toml v1.9.5 // indirect
 	github.com/pelletier/go-toml/v2 v2.2.0 // indirect
 	github.com/perimeterx/marshmallow v1.1.5 // indirect
@@ -65,7 +70,7 @@ require (
 	github.com/ugorji/go/codec v1.2.11 // indirect
 	github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect
 	go.uber.org/atomic v1.10.0 // indirect
-	go.uber.org/multierr v1.8.0 // indirect
+	go.uber.org/multierr v1.11.0 // indirect
 	golang.org/x/arch v0.7.0 // indirect
 	golang.org/x/crypto v0.23.0 // indirect
 	golang.org/x/mod v0.17.0 // indirect
diff --git a/go.sum b/go.sum
index fea24d5..a13863c 100644
--- a/go.sum
+++ b/go.sum
@@ -140,6 +140,8 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
 github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
 github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@@ -178,15 +180,21 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m
 github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
 github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM=
 github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
+github.com/hashicorp/go-plugin v1.4.4 h1:NVdrSdFRt3SkZtNckJ6tog7gbpRrcbOjQi/rgF7JYWQ=
+github.com/hashicorp/go-plugin v1.4.4/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s=
 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
 github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M=
+github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso=
 github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA=
+github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE=
+github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74=
 github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
 github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
@@ -224,6 +232,8 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k
 github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0=
+github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
 github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
 github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -241,6 +251,8 @@ github.com/oapi-codegen/oapi-codegen/v2 v2.4.1 h1:ykgG34472DWey7TSjd8vIfNykXgjOg
 github.com/oapi-codegen/oapi-codegen/v2 v2.4.1/go.mod h1:N5+lY1tiTDV3V1BeHtOxeWXHoPVeApvsvjJqegfoaz8=
 github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro=
 github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg=
+github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
+github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
@@ -317,13 +329,12 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
-go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
 go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
 go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
 go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
-go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8=
-go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
+go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
+go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
 go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY=
 go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY=
 golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
@@ -663,7 +674,6 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20191026110619-0b21df46bc1d/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/plugin/goplugin_context.go b/plugin/goplugin_context.go
new file mode 100644
index 0000000..39e58e2
--- /dev/null
+++ b/plugin/goplugin_context.go
@@ -0,0 +1,94 @@
+// Copyright 2025 Contributors to the Veraison project.
+// SPDX-License-Identifier: Apache-2.0
+package plugin
+
+import (
+	"fmt"
+	"os/exec"
+	"strings"
+
+	"github.com/hashicorp/go-plugin"
+	"github.com/veraison/services/log"
+	"go.uber.org/zap"
+)
+
+// PluginContext is a generic for handling ratsd attesters. It is
+// parameterised on the IPluggable interface it handles.
+type PluginContext struct {
+	// Path to the exectable binary containing the plugin implementation
+	Path string
+
+	// Name of this plugin
+	Name string
+
+	// Version of this plugin
+	Version string
+
+	// Handle is actual RPC interface to the plugin implementation.
+	Handle IPluggable
+
+	// go-plugin client
+	client *plugin.Client
+}
+
+func (o PluginContext) Close() {
+	if o.client != nil {
+		o.client.Kill()
+	}
+}
+
+func createPluginContext(
+	loader *GoPluginLoader,
+	path string,
+	logger *zap.SugaredLogger,
+) (*PluginContext, error) {
+	client := plugin.NewClient(
+		&plugin.ClientConfig{
+			HandshakeConfig:  handshakeConfig,
+			Plugins:          loader.pluginMap,
+			Cmd:              exec.Command(path),
+			Logger:           log.NewInternalLogger(logger),
+			AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC},
+		},
+	)
+
+	rpcClient, err := client.Client()
+	if err != nil {
+		client.Kill()
+		return nil, fmt.Errorf(
+			"unable to create the RPC client for %s: %w",
+			path, err,
+		)
+	}
+
+	protocolClient, err := rpcClient.Dispense(pluginType)
+	if err != nil {
+		client.Kill()
+		if strings.Contains(err.Error(), "unknown plugin") {
+			return nil, unknownPluginErr{Name: pluginType}
+		}
+		return nil, fmt.Errorf("unable to dispense plugin %s: %w", path, err)
+	}
+
+	handle, ok := protocolClient.(IPluggable)
+	if !ok {
+		client.Kill()
+		return nil, fmt.Errorf(
+			"failed to retrieve handle for protocol client %T",
+			protocolClient,
+		)
+	}
+
+	blob := handle.GetSubAttesterID()
+	if blob.Status.Result {
+		return nil, fmt.Errorf("failed to retrieve subattestr ID from %s", path)
+	}
+
+	return &PluginContext{
+		Path:    path,
+		Name:    blob.SubAttesterID.Name,
+		Version: blob.SubAttesterID.Version,
+		Handle:  handle,
+		client:  client,
+	}, nil
+}
diff --git a/plugin/goplugin_loader.go b/plugin/goplugin_loader.go
new file mode 100644
index 0000000..f5d3782
--- /dev/null
+++ b/plugin/goplugin_loader.go
@@ -0,0 +1,113 @@
+// Copyright 2025 Contributors to the Veraison project.
+// SPDX-License-Identifier: Apache-2.0
+package plugin
+
+import (
+	"errors"
+	"fmt"
+
+	"github.com/hashicorp/go-plugin"
+	"go.uber.org/zap"
+)
+
+type unknownPluginErr struct {
+	Name string
+}
+
+func (o unknownPluginErr) Error() string {
+	return fmt.Sprintf("unknown plugin: %s", o.Name)
+}
+
+type GoPluginLoader struct {
+	Location string
+
+	logger       *zap.SugaredLogger
+	loadedByName map[string]*PluginContext
+
+	// This gets specified as Plugins when creating a new go-plugin client.
+	pluginMap map[string]plugin.Plugin
+}
+
+func NewGoPluginLoader(logger *zap.SugaredLogger) *GoPluginLoader {
+	return &GoPluginLoader{logger: logger}
+}
+
+func CreateGoPluginLoader(
+	dir string,
+	logger *zap.SugaredLogger,
+) (*GoPluginLoader, error) {
+	loader := NewGoPluginLoader(logger)
+	err := loader.Init(dir)
+	return loader, err
+}
+
+func (o *GoPluginLoader) Init(dir string) error {
+	o.pluginMap = make(map[string]plugin.Plugin)
+	o.loadedByName = make(map[string]*PluginContext)
+	o.Location = dir
+
+	return nil
+}
+
+func (o *GoPluginLoader) Close() {
+	for _, plugin := range o.loadedByName {
+		plugin.Close()
+	}
+}
+
+func RegisterGoPluginUsing(loader *GoPluginLoader, name string) error {
+	if _, ok := loader.pluginMap[name]; ok {
+		return fmt.Errorf("plugin for %q is already registred", name)
+	}
+	loader.pluginMap[name] = &Plugin{}
+
+	return nil
+}
+
+func DiscoverGoPluginUsing(o *GoPluginLoader) error {
+	if o.Location == "" {
+		return errors.New("plugin manager has not been initialized")
+	}
+
+	o.logger.Debugw("discovering plugins", "location", o.Location)
+	pluginPaths, err := plugin.Discover("*.plugin", o.Location)
+	if err != nil {
+		return err
+	}
+
+	for _, path := range pluginPaths {
+		pluginContext, err := createPluginContext(o, path, o.logger)
+		if err != nil {
+			var upErr unknownPluginErr
+			if errors.As(err, &upErr) {
+				o.logger.Debugw("plugin not found", "name", upErr.Name, "path", path)
+				continue
+			} else {
+				return err
+			}
+		}
+
+		pluginName := pluginContext.Name
+		if existing, ok := o.loadedByName[pluginName]; ok {
+			return fmt.Errorf(
+				"plugin %q provided by two sources: [%s] and [%s]",
+				pluginName,
+				existing.Path,
+				pluginContext.Path,
+			)
+		}
+		o.loadedByName[pluginName] = pluginContext
+	}
+
+	return nil
+}
+
+func GetGoPluginHandleByNameUsing(ldr *GoPluginLoader, name string) (IPluggable, error) {
+	plugged, ok := ldr.loadedByName[name]
+	if !ok {
+		return nil, fmt.Errorf(
+			"plugin %s not found", name)
+	}
+
+	return plugged.Handle, nil
+}
diff --git a/plugin/goplugin_manager.go b/plugin/goplugin_manager.go
new file mode 100644
index 0000000..501d4ca
--- /dev/null
+++ b/plugin/goplugin_manager.go
@@ -0,0 +1,68 @@
+// Copyright 2025 Contributors to the Veraison project.
+// SPDX-License-Identifier: Apache-2.0
+package plugin
+
+import (
+	"errors"
+
+	"go.uber.org/zap"
+)
+
+var ErrNotFound = errors.New("plugin not found")
+
+type GoPluginManager struct {
+	loader *GoPluginLoader
+	logger *zap.SugaredLogger
+}
+
+func NewGoPluginManager(loader *GoPluginLoader, logger *zap.SugaredLogger) *GoPluginManager {
+	return &GoPluginManager{loader: loader, logger: logger}
+}
+
+func CreateGoPluginManager(dir string, logger *zap.SugaredLogger) (*GoPluginManager, error) {
+	loader, err := CreateGoPluginLoader(dir, logger)
+	if err != nil {
+		return nil, err
+	}
+
+	return CreateGoPluginManagerWithLoader(loader, logger)
+}
+
+func CreateGoPluginManagerWithLoader(
+	loader *GoPluginLoader,
+	logger *zap.SugaredLogger,
+) (*GoPluginManager, error) {
+	manager := NewGoPluginManager(loader, logger)
+	if err := manager.Init(); err != nil {
+		return nil, err
+	}
+
+	return manager, nil
+}
+
+func (o *GoPluginManager) Init() error {
+	err := RegisterGoPluginUsing(o.loader, pluginType)
+	if err != nil {
+		return err
+	}
+	return DiscoverGoPluginUsing(o.loader)
+}
+
+func (o *GoPluginManager) Close() error {
+	o.loader.Close()
+	return nil
+}
+
+func (o *GoPluginManager) LookupByName(name string) (IPluggable, error) {
+	return GetGoPluginHandleByNameUsing(o.loader, name)
+}
+
+func (o *GoPluginManager) GetPluginList() []string {
+	var registeredPlugin []string
+
+	for name, _ := range o.loader.loadedByName {
+		registeredPlugin = append(registeredPlugin, name)
+	}
+
+	return registeredPlugin
+}
diff --git a/plugin/goplugin_plugin.go b/plugin/goplugin_plugin.go
new file mode 100644
index 0000000..9b08a26
--- /dev/null
+++ b/plugin/goplugin_plugin.go
@@ -0,0 +1,52 @@
+// Copyright 2025 Contributors to the Veraison project.
+// SPDX-License-Identifier: Apache-2.0
+package plugin
+
+import (
+	"context"
+
+	"github.com/hashicorp/go-plugin"
+	"google.golang.org/grpc"
+
+	"github.com/veraison/ratsd/proto/compositor"
+)
+
+const (
+	pluginType = "subattester"
+)
+
+var handshakeConfig = plugin.HandshakeConfig{
+	ProtocolVersion:  1,
+	MagicCookieKey:   "RATSD_PLUGIN",
+	MagicCookieValue: "RATSD",
+}
+
+type Plugin struct {
+	plugin.Plugin
+	Impl IPluggable
+}
+
+func (p *Plugin) GRPCServer(b *plugin.GRPCBroker, s *grpc.Server) error {
+	compositor.RegisterCompositorServer(s, &GRPCServer{Impl: p.Impl})
+	return nil
+}
+
+func (p *Plugin) GRPCClient(ctx context.Context, b *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
+	return &GRPCClient{client: compositor.NewCompositorClient(c)}, nil
+}
+
+var pluginMap = map[string]plugin.Plugin{}
+
+func RegisterImplementation(i IPluggable) {
+	pluginMap[pluginType] = &Plugin{
+		Impl: i,
+	}
+}
+
+func Serve() {
+	plugin.Serve(&plugin.ServeConfig{
+		HandshakeConfig: handshakeConfig,
+		Plugins:         pluginMap,
+		GRPCServer:      plugin.DefaultGRPCServer,
+	})
+}
diff --git a/plugin/goplugin_rpc.go b/plugin/goplugin_rpc.go
new file mode 100644
index 0000000..3a14798
--- /dev/null
+++ b/plugin/goplugin_rpc.go
@@ -0,0 +1,79 @@
+// Copyright 2025 Contributors to the Veraison project.
+// SPDX-License-Identifier: Apache-2.0
+package plugin
+
+import (
+	"context"
+
+	"github.com/veraison/ratsd/proto/compositor"
+	"github.com/veraison/services/log"
+	"go.uber.org/zap"
+	"google.golang.org/protobuf/types/known/emptypb"
+)
+
+type GRPCServer struct {
+	compositor.UnimplementedCompositorServer
+	Impl IPluggable
+}
+
+func (s *GRPCServer) GetSubAttesterID(ctx context.Context, e *emptypb.Empty) (*compositor.SubAttesterIDOut, error) {
+	return s.Impl.GetSubAttesterID(), nil
+}
+
+func (s *GRPCServer) GetSupportedFormats(ctx context.Context, e *emptypb.Empty) (*compositor.SupportedFormatsOut, error) {
+	return s.Impl.GetSupportedFormats(), nil
+}
+
+func (s *GRPCServer) GetEvidence(ctx context.Context, in *compositor.EvidenceIn) (*compositor.EvidenceOut, error) {
+	return s.Impl.GetEvidence(in), nil
+}
+
+type GRPCClient struct {
+	client compositor.CompositorClient
+}
+
+func (c *GRPCClient) GetSubAttesterID() *compositor.SubAttesterIDOut {
+	resp, err := c.client.GetSubAttesterID(context.Background(), &emptypb.Empty{})
+	if err != nil {
+		return &compositor.SubAttesterIDOut{
+			Status: &compositor.Status{Result: false, Error: err.Error()},
+		}
+	}
+
+	return resp
+}
+
+func (c *GRPCClient) GetSupportedFormats() *compositor.SupportedFormatsOut {
+	resp, err := c.client.GetSupportedFormats(context.Background(), &emptypb.Empty{})
+	if err != nil {
+		return &compositor.SupportedFormatsOut{
+			Status: &compositor.Status{Result: false, Error: err.Error()},
+		}
+	}
+
+	return resp
+}
+
+func (c *GRPCClient) GetEvidence(in *compositor.EvidenceIn) *compositor.EvidenceOut {
+	resp, err := c.client.GetEvidence(context.Background(), in)
+	if err != nil {
+		return &compositor.EvidenceOut{
+			Status: &compositor.Status{Result: false, Error: err.Error()},
+		}
+	}
+
+	return resp
+}
+
+var logger *zap.SugaredLogger
+
+func init() {
+}
+
+// note: we cannot simply initialize logger
+func getLogger() *zap.SugaredLogger {
+	if logger == nil {
+		logger = log.Named("plugin.rpc")
+	}
+	return logger
+}
diff --git a/plugin/imanager.go b/plugin/imanager.go
new file mode 100644
index 0000000..6ebaf38
--- /dev/null
+++ b/plugin/imanager.go
@@ -0,0 +1,22 @@
+// Copyright 2025 Contributors to the Veraison project.
+// SPDX-License-Identifier: Apache-2.0
+package plugin
+
+// IManager defines the interface for managing plugins
+type IManager interface {
+	// Init initializes the manager, and performing plugin discovery.
+	Init() error
+
+	// Close terminates the manager.
+	Close() error
+
+	// LookupByName returns a handle (implementation of the managed
+	// interface) to the plugin with the specified name. If there is no
+	// such plugin, an error is returned.
+	LookupByName(string) (IPluggable, error)
+
+	// GetPluginList returns a []string of the name for the plugins
+	// that have been registered with the manager by discovered
+	// plugins.
+	GetPluginList() []string
+}
diff --git a/plugin/ipluggable.go b/plugin/ipluggable.go
new file mode 100644
index 0000000..30fa66d
--- /dev/null
+++ b/plugin/ipluggable.go
@@ -0,0 +1,25 @@
+// Copyright 2025 Contributors to the Veraison project.
+// SPDX-License-Identifier: Apache-2.0
+package plugin
+
+import (
+	"github.com/veraison/ratsd/proto/compositor"
+)
+
+// IPluggable respresents a "pluggable" point within Veraison ratsd.
+type IPluggable interface {
+	// GetEvidence takes *compositor.EvidenceIn as the input, which contains the nonce
+	// and one of the content type returned by GetSupportedFormats. It returns a
+	// *compositor.EvidenceOut that contains the raw evidence for this subattester as
+	// the output.
+	GetEvidence(in *compositor.EvidenceIn) *compositor.EvidenceOut
+
+	// GetSubAttesterID returns a *compositor.SubAttesterIDOut that contains
+	// the name and the version of the subattesters in field SubAttesterID 
+	GetSubAttesterID() *compositor.SubAttesterIDOut
+
+	// GetSupportedFormats returns a *compositor.SupportedFormatsOut that contains
+	// a list of the output content type and the input nonce sizes supported by this
+	// sub-attester, accessible in field Formats 
+	GetSupportedFormats() *compositor.SupportedFormatsOut
+}
diff --git a/proto/compositor/compositor.pb.go b/proto/compositor/compositor.pb.go
index 7a6970c..1a17cd0 100644
--- a/proto/compositor/compositor.pb.go
+++ b/proto/compositor/compositor.pb.go
@@ -3,8 +3,8 @@
 
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.33.0
-// 	protoc        v3.14.0
+// 	protoc-gen-go v1.31.0
+// 	protoc        v5.29.3
 // source: compositor.proto
 
 package compositor
diff --git a/proto/compositor/compositor_grpc.pb.go b/proto/compositor/compositor_grpc.pb.go
index 1f144ad..341ef0c 100644
--- a/proto/compositor/compositor_grpc.pb.go
+++ b/proto/compositor/compositor_grpc.pb.go
@@ -4,7 +4,7 @@
 // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
 // versions:
 // - protoc-gen-go-grpc v1.5.1
-// - protoc             v3.14.0
+// - protoc             v5.29.3
 // source: compositor.proto
 
 package compositor


Reviews:
user: login: jraman567
body: Thanks for the patches, @cowbon. I left a few nitpicking comments; please address them.

The PR looks good to me by and large.

I got confused by the use of "plugin" in the code. In some cases (in main.go), that refers to the plugin you implemented, but it relates to the go-plugin library within the plugin directory - just a note.

I got a chance to compare it against Veraison's implementation, and they look similar. LGTM!
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T17:28:58Z
user: login: cowbon
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T19:23:04Z
user: login: cowbon
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T19:23:17Z
user: login: cowbon
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T19:24:17Z
user: login: cowbon
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T19:28:38Z
user: login: jraman567
body: @cowbon The PR looks good to me.
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-04T19:43:23Z
user: login: thomas-fossati
body: LGTM, thanks!

I have left a couple of simple-to-address comments inline.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T10:29:41Z
user: login: cowbon
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-05T16:24:49Z
user: login: cowbon
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-05T16:25:48Z
user: login: cowbon
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-05T16:26:40Z
user: login: cowbon
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-05T16:27:30Z
user: login: thomas-fossati
body: Thanks @cowbon !
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T16:39:27Z

Review comments:
diff_hunk: @@ -0,0 +1,93 @@
+// Copyright 2025 Contributors to the Veraison project.
+// SPDX-License-Identifier: Apache-2.0
+package plugin
+
+import (
+	"fmt"
+	"os/exec"
+	"strings"
+
+	"github.com/hashicorp/go-plugin"
+	"github.com/veraison/services/log"
+	"go.uber.org/zap"
+)
+
+// PluginConntext is a generic for handling ratsd plugins. It is
+// parameterised on the IPluggale interface it handles.
+type PluginContext struct {
+	// Path to the exectable binary containing the plugin implementation
+	Path string
+	// Name of this plugin
path: plugin/goplugin_context.go
user: login: jraman567
body: nit: Inconsistent spacing. Need a new line after `Path string`
created_at: 2025-03-03T21:56:20Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,93 @@
+// Copyright 2025 Contributors to the Veraison project.
+// SPDX-License-Identifier: Apache-2.0
+package plugin
+
+import (
+	"fmt"
+	"os/exec"
+	"strings"
+
+	"github.com/hashicorp/go-plugin"
+	"github.com/veraison/services/log"
+	"go.uber.org/zap"
+)
+
+// PluginConntext is a generic for handling ratsd plugins. It is
+// parameterised on the IPluggale interface it handles.
path: plugin/goplugin_context.go
user: login: jraman567
body: nit: typo: replace IPluggale with IPluggable
created_at: 2025-03-04T15:58:54Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,93 @@
+// Copyright 2025 Contributors to the Veraison project.
+// SPDX-License-Identifier: Apache-2.0
+package plugin
+
+import (
+	"fmt"
+	"os/exec"
+	"strings"
+
+	"github.com/hashicorp/go-plugin"
+	"github.com/veraison/services/log"
+	"go.uber.org/zap"
+)
+
+// PluginConntext is a generic for handling ratsd plugins. It is
path: plugin/goplugin_context.go
user: login: jraman567
body: nit: typo: replace PluginConntext with PluginContext
created_at: 2025-03-04T15:59:29Z
author_association: COLLABORATOR
diff_hunk: @@ -60,6 +62,23 @@ func main() {
 		log.Fatalf("Could not load config: %v", err)
 	}
 
+	// Load sub-attesters from the path specified in config.yaml
+	pluginLoader, err := plugin.CreateGoPluginLoader(
+		cfg.PluginDir, log.Named("plugin"))
+
+	if err != nil {
+		log.Fatalf("could not create plugin loader: %v", err)
+	}
+
+	pluginManager, err := plugin.CreateGoPluginManagerWithLoader(
+		pluginLoader, log.Named("builtin"))
+
path: cmd/main.go
user: login: jraman567
body: Can you replace the calls to `plugin.CreateGoPluginLoader()` & `plugin.CreateGoPluginManagerWithLoader()` with a single call to `plugin.CreateGoPluginManager()` ?
created_at: 2025-03-04T17:04:47Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,93 @@
+// Copyright 2025 Contributors to the Veraison project.
+// SPDX-License-Identifier: Apache-2.0
+package plugin
+
+import (
+	"fmt"
+	"os/exec"
+	"strings"
+
+	"github.com/hashicorp/go-plugin"
+	"github.com/veraison/services/log"
+	"go.uber.org/zap"
+)
+
+// PluginConntext is a generic for handling ratsd plugins. It is
+// parameterised on the IPluggale interface it handles.
+type PluginContext struct {
+	// Path to the exectable binary containing the plugin implementation
+	Path string
+	// Name of this plugin
path: plugin/goplugin_context.go
user: login: cowbon
body: Added another line after line 19.
created_at: 2025-03-04T19:23:04Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,93 @@
+// Copyright 2025 Contributors to the Veraison project.
+// SPDX-License-Identifier: Apache-2.0
+package plugin
+
+import (
+	"fmt"
+	"os/exec"
+	"strings"
+
+	"github.com/hashicorp/go-plugin"
+	"github.com/veraison/services/log"
+	"go.uber.org/zap"
+)
+
+// PluginConntext is a generic for handling ratsd plugins. It is
+// parameterised on the IPluggale interface it handles.
path: plugin/goplugin_context.go
user: login: cowbon
body: Fixed.
created_at: 2025-03-04T19:23:17Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,93 @@
+// Copyright 2025 Contributors to the Veraison project.
+// SPDX-License-Identifier: Apache-2.0
+package plugin
+
+import (
+	"fmt"
+	"os/exec"
+	"strings"
+
+	"github.com/hashicorp/go-plugin"
+	"github.com/veraison/services/log"
+	"go.uber.org/zap"
+)
+
+// PluginConntext is a generic for handling ratsd plugins. It is
path: plugin/goplugin_context.go
user: login: cowbon
body: Removed additional alphabet "n" from `PluginConntext`. It should have the right spelling now.
created_at: 2025-03-04T19:24:17Z
author_association: COLLABORATOR
diff_hunk: @@ -60,6 +62,23 @@ func main() {
 		log.Fatalf("Could not load config: %v", err)
 	}
 
+	// Load sub-attesters from the path specified in config.yaml
+	pluginLoader, err := plugin.CreateGoPluginLoader(
+		cfg.PluginDir, log.Named("plugin"))
+
+	if err != nil {
+		log.Fatalf("could not create plugin loader: %v", err)
+	}
+
+	pluginManager, err := plugin.CreateGoPluginManagerWithLoader(
+		pluginLoader, log.Named("builtin"))
+
path: cmd/main.go
user: login: cowbon
body: Agree, there's currently no `builtin` loader here in ratsd as opposed to `veraison/services`. We might want to add a `secure loader` in ratsd as the option in the future, and we'll have to separate the loader and the manager again then. 
created_at: 2025-03-04T19:28:38Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,10 @@
+// Copyright 2025 Contributors to the Veraison project.
+// SPDX-License-Identifier: Apache-2.0
+package plugin
+
+type IManager interface {
path: plugin/imanager.go
user: login: thomas-fossati
body: This should have a godoc comment
created_at: 2025-03-05T10:18:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,16 @@
+// Copyright 2025 Contributors to the Veraison project.
+// SPDX-License-Identifier: Apache-2.0
+package plugin
+
+import (
+	"github.com/veraison/ratsd/proto/compositor"
+)
+
+// IPluggable respresents a "pluggable" point within Veraison ratsd.
+type IPluggable interface {
+	GetEvidence(in *compositor.EvidenceIn) *compositor.EvidenceOut
+
path: plugin/ipluggable.go
user: login: thomas-fossati
body: Spurious newline
created_at: 2025-03-05T10:21:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,16 @@
+// Copyright 2025 Contributors to the Veraison project.
+// SPDX-License-Identifier: Apache-2.0
+package plugin
+
+import (
+	"github.com/veraison/ratsd/proto/compositor"
+)
+
+// IPluggable respresents a "pluggable" point within Veraison ratsd.
+type IPluggable interface {
+	GetEvidence(in *compositor.EvidenceIn) *compositor.EvidenceOut
+
+	GetSubAttesterID() *compositor.SubAttesterIDOut
+
path: plugin/ipluggable.go
user: login: thomas-fossati
body: ditto
created_at: 2025-03-05T10:21:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,80 @@
+// Copyright 2025 Contributors to the Veraison project.
+// SPDX-License-Identifier: Apache-2.0
+package plugin
+
+import (
+	"context"
+
+	"github.com/veraison/services/log"
+	"go.uber.org/zap"
+	"google.golang.org/protobuf/types/known/emptypb"
+
+	"github.com/veraison/ratsd/proto/compositor"
+)
+
+type GRPCServer struct {
+	compositor.UnimplementedCompositorServer
+	Impl IPluggable
+}
+
+func (s *GRPCServer) GetSubAttesterID(ctx context.Context, e *emptypb.Empty) (*compositor.SubAttesterIDOut, error) {
+	return s.Impl.GetSubAttesterID(), nil
+}
+
+func (s *GRPCServer) GetSupportedFormats(ctx context.Context, e *emptypb.Empty) (*compositor.SupportedFormatsOut, error) {
+	return s.Impl.GetSupportedFormats(), nil
+}
+
+func (s *GRPCServer) GetEvidence(ctx context.Context, in *compositor.EvidenceIn) (*compositor.EvidenceOut, error) {
+	return s.Impl.GetEvidence(in), nil
+}
+
+type GRPCClient struct {
+	client compositor.CompositorClient
+}
+
+func (c *GRPCClient) GetSubAttesterID() *compositor.SubAttesterIDOut {
+	resp, err := c.client.GetSubAttesterID(context.Background(), &emptypb.Empty{})
+	if err != nil {
+		return &compositor.SubAttesterIDOut{
+			Status: &compositor.Status{Result: true, Error: err.Error()},
path: plugin/goplugin_rpc.go
user: login: thomas-fossati
body: On error, status.result should be false (multiple instances)
created_at: 2025-03-05T10:28:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,80 @@
+// Copyright 2025 Contributors to the Veraison project.
+// SPDX-License-Identifier: Apache-2.0
+package plugin
+
+import (
+	"context"
+
+	"github.com/veraison/services/log"
+	"go.uber.org/zap"
+	"google.golang.org/protobuf/types/known/emptypb"
+
+	"github.com/veraison/ratsd/proto/compositor"
+)
+
+type GRPCServer struct {
+	compositor.UnimplementedCompositorServer
+	Impl IPluggable
+}
+
+func (s *GRPCServer) GetSubAttesterID(ctx context.Context, e *emptypb.Empty) (*compositor.SubAttesterIDOut, error) {
+	return s.Impl.GetSubAttesterID(), nil
+}
+
+func (s *GRPCServer) GetSupportedFormats(ctx context.Context, e *emptypb.Empty) (*compositor.SupportedFormatsOut, error) {
+	return s.Impl.GetSupportedFormats(), nil
+}
+
+func (s *GRPCServer) GetEvidence(ctx context.Context, in *compositor.EvidenceIn) (*compositor.EvidenceOut, error) {
+	return s.Impl.GetEvidence(in), nil
+}
+
+type GRPCClient struct {
+	client compositor.CompositorClient
+}
+
+func (c *GRPCClient) GetSubAttesterID() *compositor.SubAttesterIDOut {
+	resp, err := c.client.GetSubAttesterID(context.Background(), &emptypb.Empty{})
+	if err != nil {
+		return &compositor.SubAttesterIDOut{
+			Status: &compositor.Status{Result: true, Error: err.Error()},
path: plugin/goplugin_rpc.go
user: login: cowbon
body: Switched the logic to `Result: false` in the latest revision.
created_at: 2025-03-05T16:24:49Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,10 @@
+// Copyright 2025 Contributors to the Veraison project.
+// SPDX-License-Identifier: Apache-2.0
+package plugin
+
+type IManager interface {
path: plugin/imanager.go
user: login: cowbon
body: Added the GoDoc similar to `veraison/service/plugin`
created_at: 2025-03-05T16:25:48Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,16 @@
+// Copyright 2025 Contributors to the Veraison project.
+// SPDX-License-Identifier: Apache-2.0
+package plugin
+
+import (
+	"github.com/veraison/ratsd/proto/compositor"
+)
+
+// IPluggable respresents a "pluggable" point within Veraison ratsd.
+type IPluggable interface {
+	GetEvidence(in *compositor.EvidenceIn) *compositor.EvidenceOut
+
path: plugin/ipluggable.go
user: login: cowbon
body: Added the GoDoc, thus retaining the newline.
created_at: 2025-03-05T16:26:40Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,16 @@
+// Copyright 2025 Contributors to the Veraison project.
+// SPDX-License-Identifier: Apache-2.0
+package plugin
+
+import (
+	"github.com/veraison/ratsd/proto/compositor"
+)
+
+// IPluggable respresents a "pluggable" point within Veraison ratsd.
+type IPluggable interface {
+	GetEvidence(in *compositor.EvidenceIn) *compositor.EvidenceOut
+
+	GetSubAttesterID() *compositor.SubAttesterIDOut
+
path: plugin/ipluggable.go
user: login: cowbon
body: Same as above
created_at: 2025-03-05T16:27:30Z
author_association: COLLABORATOR

PR comments:
user: login: cowbon
created_at: 2025-03-04T19:32:34Z
body: Hi @jraman567 Thanks for the review, note the implementations of the ratsd plugin will be called `attesters`. There will be another directory named as such to hold the attesters, e.g. TSM.
author_association: COLLABORATOR
user: login: cowbon
created_at: 2025-03-05T16:29:20Z
body: Thanks @thomas-fossati for your comments. I've added the GoDoc in the similar fashion of the `plugin` in `services`, and changed the logic of `Status.Result` when an error occurs. Please approve it if you're satisfied with the changes.
author_association: COLLABORATOR