2
0

4 Commits

Author SHA1 Message Date
7f7bdcc568 Update AIController.cs
All checks were successful
Build and Test / build (push) Successful in 28s
Release / build (push) Successful in 40s
2026-03-07 12:39:22 -05:00
6e25266da3 feat(plugins): add protocol versioning to plugin interface
All checks were successful
Build and Test / build (push) Successful in 20s
Release / build (push) Successful in 37s
Add protocol_version field to Initialize RPC for forward compatibility as the plugin protocol evolves.

Changes:
- Add protocol_version to InitializeRequest (server → plugin)
- Add protocol_version to InitializeResponse (plugin → server)
- Set current protocol version to 1
- Version 0 indicates pre-versioning implementations (treated as v1)

This allows the server and plugins to negotiate capabilities:
- Server can avoid calling RPCs that older plugins don't implement
- Plugins can detect newer servers and enable advanced features
- Graceful degradation when versions mismatch

The AI service now reports protocol version 1 during initialization.
2026-02-13 02:17:56 -05:00
ea06ca266f docs(ai-service): update readme with plugin protocol integration
Update AI service README to reflect the dual-service architecture (AI operations + plugin protocol) and complete integration guide.

Architecture Updates:
- Document both GitCaddyAIService and PluginService running on port 5000
- Add architecture diagram showing server's AI client and plugin manager connections
- Clarify h2c (cleartext HTTP/2) transport for gRPC + REST on same port

API Reference:
- Add plugin protocol RPC methods (Initialize, HealthCheck, OnEvent, Shutdown)
- Explain plugin lifecycle and health monitoring (30s intervals)
- Document manifest-based capability declaration

Integration Guide:
- Split configuration into [ai] (operations) and [plugins.gitcaddy-ai] (lifecycle)
- Explain how both sections work together for complete integration
- Update client examples to use port 5000 (was 5051)
- Add transport details and event subscription explanation

This provides a complete picture of how the AI service integrates with the server as both an AI operations provider and a managed plugin.
2026-02-13 01:54:41 -05:00
ac8aa4c868 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.
2026-02-13 01:44:55 -05:00
6 changed files with 383 additions and 17 deletions

View File

File diff suppressed because one or more lines are too long

View File

@@ -13,19 +13,30 @@ AI-powered code intelligence service for GitCaddy. Provides code review, documen
## Architecture
The AI service runs as a sidecar alongside the GitCaddy server. It exposes two gRPC services on the same port (HTTP/2 + HTTP/1.1):
- **GitCaddyAIService** - AI operations (review, triage, docs, chat) called by the server's AI client
- **PluginService** - Plugin lifecycle protocol (initialize, health, events) managed by the server's external plugin manager
```
┌─────────────────────────────────────────────────────────────────┐
│ GitCaddy Server (Go) │
── AI Client (gRPC)
┌─────────────────────────────────────────────────────────┐
│ GitCaddy AI Service (.NET 9) │
├── gRPC API
├── MarketAlly.AIPlugin (Multi-provider AI) │
│ └── License Validation
└─────────────────────────────────────────────────────────┘
└─────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────
│ GitCaddy Server (Go)
── AI Client (gRPC) ──────────────── GitCaddyAIService
│ ReviewPullRequest, TriageIssue,
ExplainCode, GenerateDocumentation │
│ │
└── External Plugin Manager (gRPC) ─── PluginService
Initialize, HealthCheck (30s), │
OnEvent, Shutdown ▼
┌──────────────────────────┐
│ GitCaddy AI (.NET 9) │
│ │ Port 5000 (h2c + HTTP) │ │
│ │ ├── AI Providers │ │
│ │ │ (Claude/OpenAI/ │ │
│ │ │ Gemini) │ │
│ │ └── License Validation │ │
│ └──────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────┘
```
## Quick Start
@@ -94,7 +105,9 @@ Environment variable format: `AIService__DefaultProvider=Claude`
## API Reference
The service exposes a gRPC API defined in `protos/gitcaddy_ai.proto`.
The service exposes two gRPC services on port 5000 (cleartext HTTP/2):
### AI Operations (`protos/gitcaddy_ai.proto`)
### Code Review
@@ -134,6 +147,21 @@ rpc ExecuteTask(ExecuteTaskRequest) returns (ExecuteTaskResponse);
rpc Chat(stream ChatRequest) returns (stream ChatResponse);
```
### Plugin Protocol (`protos/plugin.proto`)
The plugin protocol allows the GitCaddy server to manage this service as an external plugin:
```protobuf
rpc Initialize(InitializeRequest) returns (InitializeResponse);
rpc Shutdown(ShutdownRequest) returns (ShutdownResponse);
rpc HealthCheck(PluginHealthCheckRequest) returns (PluginHealthCheckResponse);
rpc GetManifest(GetManifestRequest) returns (PluginManifest);
rpc OnEvent(PluginEvent) returns (EventResponse);
rpc HandleHTTP(HTTPRequest) returns (HTTPResponse);
```
The server calls `Initialize` on startup, `HealthCheck` every 30 seconds, `OnEvent` for subscribed events (e.g., `license:updated`), and `Shutdown` on server stop. The service declares its capabilities (routes, permissions, license tier) via the `PluginManifest` returned during initialization.
## Client Libraries
### .NET Client
@@ -141,7 +169,7 @@ rpc Chat(stream ChatRequest) returns (stream ChatResponse);
```csharp
using GitCaddy.AI.Client;
var client = new GitCaddyAIClient("http://localhost:5051");
var client = new GitCaddyAIClient("http://localhost:5000");
var response = await client.ReviewPullRequestAsync(new ReviewPullRequestRequest
{
@@ -159,7 +187,7 @@ Console.WriteLine(response.Summary);
```go
import ai "git.marketally.com/gitcaddy/gitcaddy-ai/go/gitcaddy-ai-client"
client, err := ai.NewClient("localhost:5051")
client, err := ai.NewClient("localhost:5000")
if err != nil {
log.Fatal(err)
}
@@ -213,14 +241,43 @@ go generate
## Integration with GitCaddy Server
Add the AI client to your GitCaddy server configuration:
Add both sections to the server's `app.ini`:
**1. AI client configuration** (how the server calls AI operations):
```ini
[ai]
ENABLED = true
SERVICE_URL = http://localhost:5051
SERVICE_URL = localhost:5000
DEFAULT_PROVIDER = claude
DEFAULT_MODEL = claude-sonnet-4-20250514
CLAUDE_API_KEY = sk-ant-...
```
See the [server README](https://git.marketally.com/gitcaddy/gitcaddy-server#ai-features-configuration) for the full `[ai]` reference.
**2. Plugin registration** (how the server manages the sidecar's lifecycle):
```ini
[plugins]
ENABLED = true
HEALTH_CHECK_INTERVAL = 30s
[plugins.gitcaddy-ai]
ENABLED = true
ADDRESS = localhost:5000
HEALTH_TIMEOUT = 5s
SUBSCRIBED_EVENTS = license:updated
```
With both sections configured, the server will:
- Call AI operations (review, triage, etc.) via `[ai] SERVICE_URL`
- Manage the sidecar's lifecycle via the plugin protocol on `[plugins.gitcaddy-ai] ADDRESS`
- Health-check the sidecar every 30 seconds and log status changes
- Dispatch subscribed events (e.g., license updates) to the sidecar in real-time
**Transport**: All communication uses cleartext HTTP/2 (h2c). The sidecar's Kestrel server is configured for `Http1AndHttp2` on port 5000, supporting both gRPC (HTTP/2) and REST (HTTP/1.1) on the same port.
## Support
- Documentation: https://docs.gitcaddy.com/ai

107
protos/plugin.proto Normal file
View File

@@ -0,0 +1,107 @@
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;
// protocol_version is the plugin protocol version the server supports.
// The current version is 1. Plugins should check this to know what RPCs
// the server may call. A value of 0 means the server predates versioning.
int32 protocol_version = 3;
}
message InitializeResponse {
bool success = 1;
string error = 2;
PluginManifest manifest = 3;
// protocol_version is the plugin protocol version the plugin supports.
// The current version is 1. The server uses this to avoid calling RPCs
// that the plugin doesn't implement. A value of 0 means the plugin
// predates versioning and is treated as protocol version 1.
int32 protocol_version = 4;
}
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

@@ -21,6 +21,7 @@ public class AIController : ControllerBase
private readonly ICodeIntelligenceService _codeIntelligenceService;
private readonly IIssueService _issueService;
private readonly IDocumentationService _documentationService;
private readonly IWorkflowService _workflowService;
private readonly IAIProviderFactory _providerFactory;
private readonly ILicenseValidator _licenseValidator;
private readonly ILogger<AIController> _logger;
@@ -30,6 +31,7 @@ public class AIController : ControllerBase
ICodeIntelligenceService codeIntelligenceService,
IIssueService issueService,
IDocumentationService documentationService,
IWorkflowService workflowService,
IAIProviderFactory providerFactory,
ILicenseValidator licenseValidator,
ILogger<AIController> logger)
@@ -38,6 +40,7 @@ public class AIController : ControllerBase
_codeIntelligenceService = codeIntelligenceService;
_issueService = issueService;
_documentationService = documentationService;
_workflowService = workflowService;
_providerFactory = providerFactory;
_licenseValidator = licenseValidator;
_logger = logger;
@@ -490,6 +493,41 @@ public class AIController : ControllerBase
}
}
/// <summary>
/// Execute a generic AI task
/// </summary>
[HttpPost("execute-task")]
public async Task<IActionResult> ExecuteTask([FromBody] ExecuteTaskDto request, CancellationToken cancellationToken)
{
try
{
var protoRequest = new Proto.ExecuteTaskRequest
{
RepoId = request.RepoId,
Task = request.Task ?? ""
};
foreach (var kvp in request.Context ?? new Dictionary<string, string>())
{
protoRequest.Context[kvp.Key] = kvp.Value;
}
protoRequest.AllowedTools.AddRange(request.AllowedTools ?? []);
var response = await _workflowService.ExecuteTaskAsync(protoRequest, cancellationToken);
return Ok(new
{
success = response.Success,
result = response.Result,
error = response.Error
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to execute task");
return StatusCode(500, new { error = ex.Message });
}
}
private static object MapReviewResponse(Proto.ReviewPullRequestResponse response)
{
return new
@@ -659,6 +697,18 @@ public class IssueCommentDto
public string? CreatedAt { get; set; }
}
public class ExecuteTaskDto
{
[System.Text.Json.Serialization.JsonPropertyName("provider_config")]
public ProviderConfigDto? ProviderConfig { get; set; }
[System.Text.Json.Serialization.JsonPropertyName("repo_id")]
public long RepoId { get; set; }
public string? Task { get; set; }
public Dictionary<string, string>? Context { get; set; }
[System.Text.Json.Serialization.JsonPropertyName("allowed_tools")]
public List<string>? AllowedTools { get; set; }
}
public class InspectWorkflowDto
{
[System.Text.Json.Serialization.JsonPropertyName("provider_config")]

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,134 @@
// 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
{
/// <summary>
/// The plugin protocol version this implementation supports.
/// Increment when new RPCs are added to PluginService.
/// </summary>
private const int ProtocolVersion = 1;
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} (protocol v{ProtocolVersion})",
request.ServerVersion, request.ProtocolVersion);
return Task.FromResult(new InitializeResponse
{
Success = true,
Manifest = BuildManifest(),
ProtocolVersion = ProtocolVersion
});
}
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;
}
}