2
0

refactor(plugins): migrate external plugin manager to grpc/connect

Replace manual HTTP/JSON RPC implementation with generated gRPC/Connect client, providing type-safe plugin communication.

Code Generation:
- Generate plugin.pb.go and pluginv1connect/plugin.connect.go from plugin.proto
- Add generate-plugin-proto Makefile target
- Delete hand-written types.go (replaced by generated code)

ExternalPluginManager Refactoring:
- Replace httpClient with pluginv1connect.PluginServiceClient
- Use h2c (cleartext HTTP/2) transport for gRPC without TLS
- Replace all manual callRPC/callRPCWithContext calls with typed Connect methods
- Remove JSON serialization/deserialization code
- Simplify error handling with native gRPC status codes

Benefits:
- Type safety: compile-time verification of request/response types
- Protocol compatibility: standard gRPC wire format
- Reduced code: ~100 lines of manual RPC code removed
- Better errors: structured gRPC status codes instead of string parsing
- Matches existing Actions runner pattern (Connect RPC over HTTP/2)

This completes the plugin framework migration to production-grade RPC transport.
This commit is contained in:
2026-02-13 01:45:29 -05:00
parent f42c6c39f9
commit 174d18db22
7 changed files with 1552 additions and 192 deletions

View File

@@ -763,6 +763,12 @@ generate-go: $(TAGS_PREREQ)
@echo "Running go generate..."
@CC= GOOS= GOARCH= CGO_ENABLED=0 $(GO) generate -tags '$(TAGS)' ./...
.PHONY: generate-plugin-proto
generate-plugin-proto:
protoc --go_out=. --go_opt=paths=source_relative \
--connect-go_out=. --connect-go_opt=paths=source_relative \
modules/plugins/pluginv1/plugin.proto
.PHONY: security-check
security-check:
GOEXPERIMENT= go run $(GOVULNCHECK_PACKAGE) -show color ./...

View File

@@ -4,12 +4,11 @@
package plugins
import (
"bytes"
"context"
"errors"
"crypto/tls"
"fmt"
"io"
"maps"
"net"
"net/http"
"os"
"os/exec"
@@ -17,10 +16,13 @@ import (
"sync"
"time"
"connectrpc.com/connect"
"golang.org/x/net/http2"
"code.gitcaddy.com/server/v3/modules/graceful"
"code.gitcaddy.com/server/v3/modules/json"
"code.gitcaddy.com/server/v3/modules/log"
pluginv1 "code.gitcaddy.com/server/v3/modules/plugins/pluginv1"
"code.gitcaddy.com/server/v3/modules/plugins/pluginv1/pluginv1connect"
)
// PluginStatus represents the status of an external plugin
@@ -35,14 +37,14 @@ const (
// ManagedPlugin tracks the state of an external plugin
type ManagedPlugin struct {
config *ExternalPluginConfig
process *os.Process
status PluginStatus
lastSeen time.Time
manifest *pluginv1.PluginManifest
failCount int
httpClient *http.Client
mu sync.RWMutex
config *ExternalPluginConfig
process *os.Process
status PluginStatus
lastSeen time.Time
manifest *pluginv1.PluginManifest
failCount int
client pluginv1connect.PluginServiceClient
mu sync.RWMutex
}
// ExternalPluginManager manages external plugins (both managed and external mode)
@@ -85,12 +87,23 @@ func (m *ExternalPluginManager) StartAll() error {
continue
}
address := cfg.Address
if address == "" {
log.Error("External plugin %s has no address configured", name)
continue
}
if !strings.HasPrefix(address, "http://") && !strings.HasPrefix(address, "https://") {
address = "http://" + address
}
mp := &ManagedPlugin{
config: cfg,
status: PluginStatusStarting,
httpClient: &http.Client{
Timeout: cfg.HealthTimeout,
},
client: pluginv1connect.NewPluginServiceClient(
newH2CClient(cfg.HealthTimeout),
address,
connect.WithGRPC(),
),
}
m.plugins[name] = mp
@@ -127,7 +140,7 @@ func (m *ExternalPluginManager) StopAll() {
for name, mp := range m.plugins {
log.Info("Shutting down external plugin: %s", name)
// Send shutdown request
// Send shutdown request via Connect RPC
m.shutdownPlugin(mp)
// Kill managed process
@@ -190,8 +203,13 @@ func (m *ExternalPluginManager) OnEvent(event *pluginv1.PluginEvent) {
ctx, cancel := context.WithTimeout(m.ctx, 30*time.Second)
defer cancel()
if err := m.callOnEvent(ctx, p, event); err != nil {
resp, err := p.client.OnEvent(ctx, connect.NewRequest(event))
if err != nil {
log.Error("Failed to dispatch event %s to plugin %s: %v", event.EventType, pluginName, err)
return
}
if resp.Msg.Error != "" {
log.Error("Plugin %s returned error for event %s: %s", pluginName, event.EventType, resp.Msg.Error)
}
}(name, mp)
}
@@ -210,22 +228,22 @@ func (m *ExternalPluginManager) HandleHTTP(method, path string, headers map[stri
}
for _, route := range mp.manifest.Routes {
if route.Method == method && matchRoute(route.Path, path) {
if route.Method == method && strings.HasPrefix(path, route.Path) {
mp.mu.RUnlock()
ctx, cancel := context.WithTimeout(m.ctx, 30*time.Second)
defer cancel()
resp, err := m.callHandleHTTP(ctx, mp, &pluginv1.HTTPRequest{
resp, err := mp.client.HandleHTTP(ctx, connect.NewRequest(&pluginv1.HTTPRequest{
Method: method,
Path: path,
Headers: headers,
Body: body,
})
}))
if err != nil {
return nil, fmt.Errorf("plugin %s HandleHTTP failed: %w", name, err)
}
return resp, nil
return resp.Msg, nil
}
}
mp.mu.RUnlock()
@@ -276,107 +294,45 @@ func (m *ExternalPluginManager) startManagedPlugin(mp *ManagedPlugin) error {
}
func (m *ExternalPluginManager) initializePlugin(mp *ManagedPlugin) error {
req := &pluginv1.InitializeRequest{
resp, err := mp.client.Initialize(m.ctx, connect.NewRequest(&pluginv1.InitializeRequest{
ServerVersion: "3.0.0",
Config: map[string]string{},
}))
if err != nil {
return fmt.Errorf("plugin Initialize RPC failed: %w", err)
}
resp := &pluginv1.InitializeResponse{}
if err := m.callRPC(mp, "initialize", req, resp); err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("plugin initialization failed: %s", resp.Error)
if !resp.Msg.Success {
return fmt.Errorf("plugin initialization failed: %s", resp.Msg.Error)
}
mp.mu.Lock()
mp.manifest = resp.Manifest
mp.manifest = resp.Msg.Manifest
mp.mu.Unlock()
return nil
}
func (m *ExternalPluginManager) shutdownPlugin(mp *ManagedPlugin) {
req := &pluginv1.ShutdownRequest{Reason: "server shutdown"}
resp := &pluginv1.ShutdownResponse{}
if err := m.callRPC(mp, "shutdown", req, resp); err != nil {
_, err := mp.client.Shutdown(m.ctx, connect.NewRequest(&pluginv1.ShutdownRequest{
Reason: "server shutdown",
}))
if err != nil {
log.Warn("Plugin shutdown call failed: %v", err)
}
}
func (m *ExternalPluginManager) callOnEvent(ctx context.Context, mp *ManagedPlugin, event *pluginv1.PluginEvent) error {
resp := &pluginv1.EventResponse{}
if err := m.callRPCWithContext(ctx, mp, "on-event", event, resp); err != nil {
return err
// newH2CClient creates an HTTP client that supports cleartext HTTP/2 (h2c)
// for communicating with gRPC services without TLS.
func newH2CClient(timeout time.Duration) *http.Client {
return &http.Client{
Timeout: timeout,
Transport: &http2.Transport{
AllowHTTP: true,
DialTLSContext: func(ctx context.Context, network, addr string, _ *tls.Config) (net.Conn, error) {
var d net.Dialer
return d.DialContext(ctx, network, addr)
},
},
}
if resp.Error != "" {
return fmt.Errorf("plugin event error: %s", resp.Error)
}
return nil
}
func (m *ExternalPluginManager) callHandleHTTP(ctx context.Context, mp *ManagedPlugin, req *pluginv1.HTTPRequest) (*pluginv1.HTTPResponse, error) {
resp := &pluginv1.HTTPResponse{}
if err := m.callRPCWithContext(ctx, mp, "handle-http", req, resp); err != nil {
return nil, err
}
return resp, nil
}
// callRPC makes a JSON-over-HTTP call to the plugin (simplified RPC)
func (m *ExternalPluginManager) callRPC(mp *ManagedPlugin, method string, req, resp any) error {
return m.callRPCWithContext(m.ctx, mp, method, req, resp)
}
func (m *ExternalPluginManager) callRPCWithContext(ctx context.Context, mp *ManagedPlugin, method string, reqBody, respBody any) error {
address := mp.config.Address
if address == "" {
return errors.New("plugin has no address configured")
}
// Ensure address has scheme
if !strings.HasPrefix(address, "http://") && !strings.HasPrefix(address, "https://") {
address = "http://" + address
}
url := address + "/plugin/v1/" + method
body, err := json.Marshal(reqBody)
if err != nil {
return fmt.Errorf("failed to marshal request: %w", err)
}
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
httpResp, err := mp.httpClient.Do(httpReq)
if err != nil {
return fmt.Errorf("RPC call to %s failed: %w", method, err)
}
defer httpResp.Body.Close()
respData, err := io.ReadAll(httpResp.Body)
if err != nil {
return fmt.Errorf("failed to read response: %w", err)
}
if httpResp.StatusCode != http.StatusOK {
return fmt.Errorf("RPC %s returned status %d: %s", method, httpResp.StatusCode, string(respData))
}
if err := json.Unmarshal(respData, respBody); err != nil {
return fmt.Errorf("failed to unmarshal response: %w", err)
}
return nil
}
// matchRoute checks if a URL path matches a route pattern (simple prefix matching)
func matchRoute(pattern, path string) bool {
// Simple prefix match for now
return strings.HasPrefix(path, pattern)
}

View File

@@ -8,6 +8,8 @@ import (
"maps"
"time"
"connectrpc.com/connect"
"code.gitcaddy.com/server/v3/modules/graceful"
"code.gitcaddy.com/server/v3/modules/log"
pluginv1 "code.gitcaddy.com/server/v3/modules/plugins/pluginv1"
@@ -59,8 +61,7 @@ func (m *ExternalPluginManager) checkPlugin(ctx context.Context, name string, mp
healthCtx, cancel := context.WithTimeout(ctx, mp.config.HealthTimeout)
defer cancel()
resp := &pluginv1.HealthCheckResponse{}
err := m.callRPCWithContext(healthCtx, mp, "health-check", &pluginv1.HealthCheckRequest{}, resp)
resp, err := mp.client.HealthCheck(healthCtx, connect.NewRequest(&pluginv1.HealthCheckRequest{}))
mp.mu.Lock()
defer mp.mu.Unlock()
@@ -90,8 +91,8 @@ func (m *ExternalPluginManager) checkPlugin(ctx context.Context, name string, mp
mp.status = PluginStatusOnline
mp.lastSeen = time.Now()
if !resp.Healthy {
log.Warn("Plugin %s reports unhealthy: %s", name, resp.Status)
if !resp.Msg.Healthy {
log.Warn("Plugin %s reports unhealthy: %s", name, resp.Msg.Status)
mp.status = PluginStatusError
}

1214
modules/plugins/pluginv1/plugin.pb.go generated Normal file
View File

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@ syntax = "proto3";
package plugin.v1;
option go_package = "code.gitcaddy.com/server/v3/modules/plugins/pluginv1";
option go_package = "code.gitcaddy.com/server/v3/modules/plugins/pluginv1;pluginv1";
import "google/protobuf/struct.proto";
import "google/protobuf/timestamp.proto";

View File

@@ -0,0 +1,264 @@
// Code generated by protoc-gen-connect-go. DO NOT EDIT.
//
// Source: modules/plugins/pluginv1/plugin.proto
package pluginv1connect
import (
pluginv1 "code.gitcaddy.com/server/v3/modules/plugins/pluginv1"
connect "connectrpc.com/connect"
context "context"
errors "errors"
http "net/http"
strings "strings"
)
// This is a compile-time assertion to ensure that this generated file and the connect package are
// compatible. If you get a compiler error that this constant is not defined, this code was
// generated with a version of connect newer than the one compiled into your binary. You can fix the
// problem by either regenerating this code with an older version of connect or updating the connect
// version compiled into your binary.
const _ = connect.IsAtLeastVersion1_13_0
const (
// PluginServiceName is the fully-qualified name of the PluginService service.
PluginServiceName = "plugin.v1.PluginService"
)
// These constants are the fully-qualified names of the RPCs defined in this package. They're
// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route.
//
// Note that these are different from the fully-qualified method names used by
// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to
// reflection-formatted method names, remove the leading slash and convert the remaining slash to a
// period.
const (
// PluginServiceInitializeProcedure is the fully-qualified name of the PluginService's Initialize
// RPC.
PluginServiceInitializeProcedure = "/plugin.v1.PluginService/Initialize"
// PluginServiceShutdownProcedure is the fully-qualified name of the PluginService's Shutdown RPC.
PluginServiceShutdownProcedure = "/plugin.v1.PluginService/Shutdown"
// PluginServiceHealthCheckProcedure is the fully-qualified name of the PluginService's HealthCheck
// RPC.
PluginServiceHealthCheckProcedure = "/plugin.v1.PluginService/HealthCheck"
// PluginServiceGetManifestProcedure is the fully-qualified name of the PluginService's GetManifest
// RPC.
PluginServiceGetManifestProcedure = "/plugin.v1.PluginService/GetManifest"
// PluginServiceOnEventProcedure is the fully-qualified name of the PluginService's OnEvent RPC.
PluginServiceOnEventProcedure = "/plugin.v1.PluginService/OnEvent"
// PluginServiceHandleHTTPProcedure is the fully-qualified name of the PluginService's HandleHTTP
// RPC.
PluginServiceHandleHTTPProcedure = "/plugin.v1.PluginService/HandleHTTP"
)
// PluginServiceClient is a client for the plugin.v1.PluginService service.
type PluginServiceClient interface {
// Initialize is called when the server starts or the plugin is loaded
Initialize(context.Context, *connect.Request[pluginv1.InitializeRequest]) (*connect.Response[pluginv1.InitializeResponse], error)
// Shutdown is called when the server is shutting down
Shutdown(context.Context, *connect.Request[pluginv1.ShutdownRequest]) (*connect.Response[pluginv1.ShutdownResponse], error)
// HealthCheck checks if the plugin is healthy
HealthCheck(context.Context, *connect.Request[pluginv1.HealthCheckRequest]) (*connect.Response[pluginv1.HealthCheckResponse], error)
// GetManifest returns the plugin's manifest describing its capabilities
GetManifest(context.Context, *connect.Request[pluginv1.GetManifestRequest]) (*connect.Response[pluginv1.PluginManifest], error)
// OnEvent is called when an event the plugin is subscribed to occurs
OnEvent(context.Context, *connect.Request[pluginv1.PluginEvent]) (*connect.Response[pluginv1.EventResponse], error)
// HandleHTTP proxies an HTTP request to the plugin
HandleHTTP(context.Context, *connect.Request[pluginv1.HTTPRequest]) (*connect.Response[pluginv1.HTTPResponse], error)
}
// NewPluginServiceClient constructs a client for the plugin.v1.PluginService service. By default,
// it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and
// sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC()
// or connect.WithGRPCWeb() options.
//
// The URL supplied here should be the base URL for the Connect or gRPC server (for example,
// http://api.acme.com or https://acme.com/grpc).
func NewPluginServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) PluginServiceClient {
baseURL = strings.TrimRight(baseURL, "/")
pluginServiceMethods := pluginv1.File_modules_plugins_pluginv1_plugin_proto.Services().ByName("PluginService").Methods()
return &pluginServiceClient{
initialize: connect.NewClient[pluginv1.InitializeRequest, pluginv1.InitializeResponse](
httpClient,
baseURL+PluginServiceInitializeProcedure,
connect.WithSchema(pluginServiceMethods.ByName("Initialize")),
connect.WithClientOptions(opts...),
),
shutdown: connect.NewClient[pluginv1.ShutdownRequest, pluginv1.ShutdownResponse](
httpClient,
baseURL+PluginServiceShutdownProcedure,
connect.WithSchema(pluginServiceMethods.ByName("Shutdown")),
connect.WithClientOptions(opts...),
),
healthCheck: connect.NewClient[pluginv1.HealthCheckRequest, pluginv1.HealthCheckResponse](
httpClient,
baseURL+PluginServiceHealthCheckProcedure,
connect.WithSchema(pluginServiceMethods.ByName("HealthCheck")),
connect.WithClientOptions(opts...),
),
getManifest: connect.NewClient[pluginv1.GetManifestRequest, pluginv1.PluginManifest](
httpClient,
baseURL+PluginServiceGetManifestProcedure,
connect.WithSchema(pluginServiceMethods.ByName("GetManifest")),
connect.WithClientOptions(opts...),
),
onEvent: connect.NewClient[pluginv1.PluginEvent, pluginv1.EventResponse](
httpClient,
baseURL+PluginServiceOnEventProcedure,
connect.WithSchema(pluginServiceMethods.ByName("OnEvent")),
connect.WithClientOptions(opts...),
),
handleHTTP: connect.NewClient[pluginv1.HTTPRequest, pluginv1.HTTPResponse](
httpClient,
baseURL+PluginServiceHandleHTTPProcedure,
connect.WithSchema(pluginServiceMethods.ByName("HandleHTTP")),
connect.WithClientOptions(opts...),
),
}
}
// pluginServiceClient implements PluginServiceClient.
type pluginServiceClient struct {
initialize *connect.Client[pluginv1.InitializeRequest, pluginv1.InitializeResponse]
shutdown *connect.Client[pluginv1.ShutdownRequest, pluginv1.ShutdownResponse]
healthCheck *connect.Client[pluginv1.HealthCheckRequest, pluginv1.HealthCheckResponse]
getManifest *connect.Client[pluginv1.GetManifestRequest, pluginv1.PluginManifest]
onEvent *connect.Client[pluginv1.PluginEvent, pluginv1.EventResponse]
handleHTTP *connect.Client[pluginv1.HTTPRequest, pluginv1.HTTPResponse]
}
// Initialize calls plugin.v1.PluginService.Initialize.
func (c *pluginServiceClient) Initialize(ctx context.Context, req *connect.Request[pluginv1.InitializeRequest]) (*connect.Response[pluginv1.InitializeResponse], error) {
return c.initialize.CallUnary(ctx, req)
}
// Shutdown calls plugin.v1.PluginService.Shutdown.
func (c *pluginServiceClient) Shutdown(ctx context.Context, req *connect.Request[pluginv1.ShutdownRequest]) (*connect.Response[pluginv1.ShutdownResponse], error) {
return c.shutdown.CallUnary(ctx, req)
}
// HealthCheck calls plugin.v1.PluginService.HealthCheck.
func (c *pluginServiceClient) HealthCheck(ctx context.Context, req *connect.Request[pluginv1.HealthCheckRequest]) (*connect.Response[pluginv1.HealthCheckResponse], error) {
return c.healthCheck.CallUnary(ctx, req)
}
// GetManifest calls plugin.v1.PluginService.GetManifest.
func (c *pluginServiceClient) GetManifest(ctx context.Context, req *connect.Request[pluginv1.GetManifestRequest]) (*connect.Response[pluginv1.PluginManifest], error) {
return c.getManifest.CallUnary(ctx, req)
}
// OnEvent calls plugin.v1.PluginService.OnEvent.
func (c *pluginServiceClient) OnEvent(ctx context.Context, req *connect.Request[pluginv1.PluginEvent]) (*connect.Response[pluginv1.EventResponse], error) {
return c.onEvent.CallUnary(ctx, req)
}
// HandleHTTP calls plugin.v1.PluginService.HandleHTTP.
func (c *pluginServiceClient) HandleHTTP(ctx context.Context, req *connect.Request[pluginv1.HTTPRequest]) (*connect.Response[pluginv1.HTTPResponse], error) {
return c.handleHTTP.CallUnary(ctx, req)
}
// PluginServiceHandler is an implementation of the plugin.v1.PluginService service.
type PluginServiceHandler interface {
// Initialize is called when the server starts or the plugin is loaded
Initialize(context.Context, *connect.Request[pluginv1.InitializeRequest]) (*connect.Response[pluginv1.InitializeResponse], error)
// Shutdown is called when the server is shutting down
Shutdown(context.Context, *connect.Request[pluginv1.ShutdownRequest]) (*connect.Response[pluginv1.ShutdownResponse], error)
// HealthCheck checks if the plugin is healthy
HealthCheck(context.Context, *connect.Request[pluginv1.HealthCheckRequest]) (*connect.Response[pluginv1.HealthCheckResponse], error)
// GetManifest returns the plugin's manifest describing its capabilities
GetManifest(context.Context, *connect.Request[pluginv1.GetManifestRequest]) (*connect.Response[pluginv1.PluginManifest], error)
// OnEvent is called when an event the plugin is subscribed to occurs
OnEvent(context.Context, *connect.Request[pluginv1.PluginEvent]) (*connect.Response[pluginv1.EventResponse], error)
// HandleHTTP proxies an HTTP request to the plugin
HandleHTTP(context.Context, *connect.Request[pluginv1.HTTPRequest]) (*connect.Response[pluginv1.HTTPResponse], error)
}
// NewPluginServiceHandler builds an HTTP handler from the service implementation. It returns the
// path on which to mount the handler and the handler itself.
//
// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf
// and JSON codecs. They also support gzip compression.
func NewPluginServiceHandler(svc PluginServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) {
pluginServiceMethods := pluginv1.File_modules_plugins_pluginv1_plugin_proto.Services().ByName("PluginService").Methods()
pluginServiceInitializeHandler := connect.NewUnaryHandler(
PluginServiceInitializeProcedure,
svc.Initialize,
connect.WithSchema(pluginServiceMethods.ByName("Initialize")),
connect.WithHandlerOptions(opts...),
)
pluginServiceShutdownHandler := connect.NewUnaryHandler(
PluginServiceShutdownProcedure,
svc.Shutdown,
connect.WithSchema(pluginServiceMethods.ByName("Shutdown")),
connect.WithHandlerOptions(opts...),
)
pluginServiceHealthCheckHandler := connect.NewUnaryHandler(
PluginServiceHealthCheckProcedure,
svc.HealthCheck,
connect.WithSchema(pluginServiceMethods.ByName("HealthCheck")),
connect.WithHandlerOptions(opts...),
)
pluginServiceGetManifestHandler := connect.NewUnaryHandler(
PluginServiceGetManifestProcedure,
svc.GetManifest,
connect.WithSchema(pluginServiceMethods.ByName("GetManifest")),
connect.WithHandlerOptions(opts...),
)
pluginServiceOnEventHandler := connect.NewUnaryHandler(
PluginServiceOnEventProcedure,
svc.OnEvent,
connect.WithSchema(pluginServiceMethods.ByName("OnEvent")),
connect.WithHandlerOptions(opts...),
)
pluginServiceHandleHTTPHandler := connect.NewUnaryHandler(
PluginServiceHandleHTTPProcedure,
svc.HandleHTTP,
connect.WithSchema(pluginServiceMethods.ByName("HandleHTTP")),
connect.WithHandlerOptions(opts...),
)
return "/plugin.v1.PluginService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case PluginServiceInitializeProcedure:
pluginServiceInitializeHandler.ServeHTTP(w, r)
case PluginServiceShutdownProcedure:
pluginServiceShutdownHandler.ServeHTTP(w, r)
case PluginServiceHealthCheckProcedure:
pluginServiceHealthCheckHandler.ServeHTTP(w, r)
case PluginServiceGetManifestProcedure:
pluginServiceGetManifestHandler.ServeHTTP(w, r)
case PluginServiceOnEventProcedure:
pluginServiceOnEventHandler.ServeHTTP(w, r)
case PluginServiceHandleHTTPProcedure:
pluginServiceHandleHTTPHandler.ServeHTTP(w, r)
default:
http.NotFound(w, r)
}
})
}
// UnimplementedPluginServiceHandler returns CodeUnimplemented from all methods.
type UnimplementedPluginServiceHandler struct{}
func (UnimplementedPluginServiceHandler) Initialize(context.Context, *connect.Request[pluginv1.InitializeRequest]) (*connect.Response[pluginv1.InitializeResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("plugin.v1.PluginService.Initialize is not implemented"))
}
func (UnimplementedPluginServiceHandler) Shutdown(context.Context, *connect.Request[pluginv1.ShutdownRequest]) (*connect.Response[pluginv1.ShutdownResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("plugin.v1.PluginService.Shutdown is not implemented"))
}
func (UnimplementedPluginServiceHandler) HealthCheck(context.Context, *connect.Request[pluginv1.HealthCheckRequest]) (*connect.Response[pluginv1.HealthCheckResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("plugin.v1.PluginService.HealthCheck is not implemented"))
}
func (UnimplementedPluginServiceHandler) GetManifest(context.Context, *connect.Request[pluginv1.GetManifestRequest]) (*connect.Response[pluginv1.PluginManifest], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("plugin.v1.PluginService.GetManifest is not implemented"))
}
func (UnimplementedPluginServiceHandler) OnEvent(context.Context, *connect.Request[pluginv1.PluginEvent]) (*connect.Response[pluginv1.EventResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("plugin.v1.PluginService.OnEvent is not implemented"))
}
func (UnimplementedPluginServiceHandler) HandleHTTP(context.Context, *connect.Request[pluginv1.HTTPRequest]) (*connect.Response[pluginv1.HTTPResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("plugin.v1.PluginService.HandleHTTP is not implemented"))
}

View File

@@ -1,81 +0,0 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT
// Package pluginv1 defines the plugin service contract types.
// These types mirror the plugin.proto definitions and will be replaced
// by generated code when protoc-gen-go and protoc-gen-connect-go are run.
package pluginv1
import "time"
type InitializeRequest struct {
ServerVersion string `json:"server_version"`
Config map[string]string `json:"config"`
}
type InitializeResponse struct {
Success bool `json:"success"`
Error string `json:"error"`
Manifest *PluginManifest `json:"manifest"`
}
type ShutdownRequest struct {
Reason string `json:"reason"`
}
type ShutdownResponse struct {
Success bool `json:"success"`
}
type HealthCheckRequest struct{}
type HealthCheckResponse struct {
Healthy bool `json:"healthy"`
Status string `json:"status"`
Details map[string]string `json:"details"`
}
type GetManifestRequest struct{}
type PluginManifest struct {
Name string `json:"name"`
Version string `json:"version"`
Description string `json:"description"`
SubscribedEvents []string `json:"subscribed_events"`
Routes []PluginRoute `json:"routes"`
RequiredPermissions []string `json:"required_permissions"`
LicenseTier string `json:"license_tier"`
}
type PluginRoute struct {
Method string `json:"method"`
Path string `json:"path"`
Description string `json:"description"`
}
type PluginEvent struct {
EventType string `json:"event_type"`
Payload map[string]any `json:"payload"`
Timestamp time.Time `json:"timestamp"`
RepoID int64 `json:"repo_id"`
OrgID int64 `json:"org_id"`
}
type EventResponse struct {
Handled bool `json:"handled"`
Error string `json:"error"`
}
type HTTPRequest struct {
Method string `json:"method"`
Path string `json:"path"`
Headers map[string]string `json:"headers"`
Body []byte `json:"body"`
QueryParams map[string]string `json:"query_params"`
}
type HTTPResponse struct {
StatusCode int `json:"status_code"`
Headers map[string]string `json:"headers"`
Body []byte `json:"body"`
}