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:
8
.notes/note-1770964636626-3yh3ujrlr.json
Normal file
8
.notes/note-1770964636626-3yh3ujrlr.json
Normal file
File diff suppressed because one or more lines are too long
98
protos/plugin.proto
Normal file
98
protos/plugin.proto
Normal 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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
125
src/GitCaddy.AI.Service/Services/PluginServiceImpl.cs
Normal file
125
src/GitCaddy.AI.Service/Services/PluginServiceImpl.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user