2
0

feat(ai-service): implement plugin protocol for managed lifecycle

Add gRPC-based plugin protocol implementation to the AI sidecar, enabling the GitCaddy server to manage it as an external plugin with lifecycle control and health monitoring.

Plugin Protocol Implementation:
- Add plugin.proto with PluginService definition (Initialize, Shutdown, HealthCheck, GetManifest, OnEvent, HandleHTTP)
- Implement PluginServiceImpl gRPC service in C#
- Return manifest declaring AI service capabilities, routes, and required permissions
- Integrate license validation into health checks
- Register plugin service alongside existing AI service

Server Integration:
- Configure Kestrel for HTTP/1.1 + HTTP/2 on port 5000 (enables gRPC + REST)
- Map PluginService gRPC endpoint at /plugin.v1.PluginService
- Enable server to call Initialize on startup, HealthCheck periodically, and Shutdown on graceful stop

This completes Phase 5 of the AI integration, allowing the server's external plugin manager to monitor and control the sidecar's lifecycle instead of relying on manual process management.
This commit is contained in:
2026-02-13 01:44:55 -05:00
parent a1bc4faef4
commit ac8aa4c868
4 changed files with 241 additions and 0 deletions

View File

File diff suppressed because one or more lines are too long

98
protos/plugin.proto Normal file
View File

@@ -0,0 +1,98 @@
syntax = "proto3";
package plugin.v1;
option go_package = "code.gitcaddy.com/server/v3/modules/plugins/pluginv1;pluginv1";
option csharp_namespace = "GitCaddy.AI.Plugin.Proto";
import "google/protobuf/struct.proto";
import "google/protobuf/timestamp.proto";
// PluginService is the RPC interface that external plugins must implement.
// The server calls these methods to manage the plugin's lifecycle and dispatch events.
service PluginService {
// Initialize is called when the server starts or the plugin is loaded
rpc Initialize(InitializeRequest) returns (InitializeResponse);
// Shutdown is called when the server is shutting down
rpc Shutdown(ShutdownRequest) returns (ShutdownResponse);
// HealthCheck checks if the plugin is healthy
rpc HealthCheck(PluginHealthCheckRequest) returns (PluginHealthCheckResponse);
// GetManifest returns the plugin's manifest describing its capabilities
rpc GetManifest(GetManifestRequest) returns (PluginManifest);
// OnEvent is called when an event the plugin is subscribed to occurs
rpc OnEvent(PluginEvent) returns (EventResponse);
// HandleHTTP proxies an HTTP request to the plugin
rpc HandleHTTP(HTTPRequest) returns (HTTPResponse);
}
message InitializeRequest {
string server_version = 1;
map<string, string> config = 2;
}
message InitializeResponse {
bool success = 1;
string error = 2;
PluginManifest manifest = 3;
}
message ShutdownRequest {
string reason = 1;
}
message ShutdownResponse {
bool success = 1;
}
message PluginHealthCheckRequest {}
message PluginHealthCheckResponse {
bool healthy = 1;
string status = 2;
map<string, string> details = 3;
}
message GetManifestRequest {}
message PluginManifest {
string name = 1;
string version = 2;
string description = 3;
repeated string subscribed_events = 4;
repeated PluginRoute routes = 5;
repeated string required_permissions = 6;
string license_tier = 7;
}
message PluginRoute {
string method = 1;
string path = 2;
string description = 3;
}
message PluginEvent {
string event_type = 1;
google.protobuf.Struct payload = 2;
google.protobuf.Timestamp timestamp = 3;
int64 repo_id = 4;
int64 org_id = 5;
}
message EventResponse {
bool handled = 1;
string error = 2;
}
message HTTPRequest {
string method = 1;
string path = 2;
map<string, string> headers = 3;
bytes body = 4;
map<string, string> query_params = 5;
}
message HTTPResponse {
int32 status_code = 1;
map<string, string> headers = 2;
bytes body = 3;
}

View File

@@ -32,6 +32,7 @@ builder.Services.AddSingleton<IIssueService, IssueService>();
builder.Services.AddSingleton<IDocumentationService, DocumentationService>();
builder.Services.AddSingleton<IWorkflowService, WorkflowService>();
builder.Services.AddSingleton<IChatService, ChatService>();
builder.Services.AddSingleton<PluginServiceImpl>();
// Add gRPC
builder.Services.AddGrpc(options =>
@@ -48,6 +49,14 @@ builder.Services.AddEndpointsApiExplorer();
// Add health checks
builder.Services.AddHealthChecks();
builder.WebHost.ConfigureKestrel(options =>
{
options.ListenAnyIP(5000, listenOptions =>
{
listenOptions.Protocols = Microsoft.AspNetCore.Server.Kestrel.Core.HttpProtocols.Http1AndHttp2;
});
});
var app = builder.Build();
// Validate license on startup
@@ -65,6 +74,7 @@ else
// Map gRPC services
app.MapGrpcService<GitCaddyAIServiceImpl>();
app.MapGrpcService<PluginServiceImpl>();
app.MapHealthChecks("/healthz");
// Map REST API controllers

View File

@@ -0,0 +1,125 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT
using Grpc.Core;
using GitCaddy.AI.Plugin.Proto;
using GitCaddy.AI.Service.Licensing;
namespace GitCaddy.AI.Service.Services;
/// <summary>
/// gRPC service implementation for the GitCaddy plugin protocol.
/// Allows the GitCaddy server to manage this sidecar as a plugin.
/// </summary>
public class PluginServiceImpl : PluginService.PluginServiceBase
{
private readonly ILogger<PluginServiceImpl> _logger;
private readonly ILicenseValidator _licenseValidator;
public PluginServiceImpl(
ILicenseValidator licenseValidator,
ILogger<PluginServiceImpl> logger)
{
_licenseValidator = licenseValidator;
_logger = logger;
}
public override Task<InitializeResponse> Initialize(
InitializeRequest request, ServerCallContext context)
{
_logger.LogInformation("Plugin initialized by server version {ServerVersion}", request.ServerVersion);
return Task.FromResult(new InitializeResponse
{
Success = true,
Manifest = BuildManifest()
});
}
public override Task<ShutdownResponse> Shutdown(
ShutdownRequest request, ServerCallContext context)
{
_logger.LogInformation("Plugin shutdown requested: {Reason}", request.Reason);
return Task.FromResult(new ShutdownResponse
{
Success = true
});
}
public override async Task<PluginHealthCheckResponse> HealthCheck(
PluginHealthCheckRequest request, ServerCallContext context)
{
var licenseResult = await _licenseValidator.ValidateAsync();
var version = typeof(PluginServiceImpl).Assembly.GetName().Version?.ToString() ?? "1.0.0";
var response = new PluginHealthCheckResponse
{
Healthy = licenseResult.IsValid,
Status = licenseResult.IsValid ? "healthy" : "degraded"
};
response.Details["version"] = version;
response.Details["license_tier"] = licenseResult.License?.Tier ?? "none";
return response;
}
public override Task<PluginManifest> GetManifest(
GetManifestRequest request, ServerCallContext context)
{
return Task.FromResult(BuildManifest());
}
public override Task<EventResponse> OnEvent(
PluginEvent request, ServerCallContext context)
{
_logger.LogInformation("Received event {EventType} for repo {RepoId}", request.EventType, request.RepoId);
return Task.FromResult(new EventResponse
{
Handled = true
});
}
public override Task<HTTPResponse> HandleHTTP(
HTTPRequest request, ServerCallContext context)
{
// Not used — the sidecar is called directly via REST by the server.
return Task.FromResult(new HTTPResponse
{
StatusCode = 501
});
}
private static PluginManifest BuildManifest()
{
var version = typeof(PluginServiceImpl).Assembly.GetName().Version?.ToString() ?? "1.0.0";
var manifest = new PluginManifest
{
Name = "GitCaddy AI Service",
Version = version,
Description = "AI-powered code intelligence service for GitCaddy",
LicenseTier = "professional"
};
manifest.SubscribedEvents.Add("license:updated");
manifest.RequiredPermissions.Add("ai:*");
manifest.Routes.AddRange(new[]
{
new PluginRoute { Method = "POST", Path = "/api/v1/review/pull-request", Description = "Review a pull request" },
new PluginRoute { Method = "POST", Path = "/api/v1/issues/triage", Description = "Triage an issue" },
new PluginRoute { Method = "POST", Path = "/api/v1/issues/suggest-labels", Description = "Suggest labels for an issue" },
new PluginRoute { Method = "POST", Path = "/api/v1/code/explain", Description = "Explain code" },
new PluginRoute { Method = "POST", Path = "/api/v1/docs/generate", Description = "Generate documentation" },
new PluginRoute { Method = "POST", Path = "/api/v1/docs/commit-message", Description = "Generate commit message" },
new PluginRoute { Method = "POST", Path = "/api/v1/issues/respond", Description = "Generate a response to an issue" },
new PluginRoute { Method = "POST", Path = "/api/v1/workflows/inspect", Description = "Inspect a workflow YAML file" },
new PluginRoute { Method = "GET", Path = "/api/v1/health", Description = "Health check" }
});
return manifest;
}
}