2
0

4 Commits

Author SHA1 Message Date
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
a1bc4faef4 feat(ai-service): add per-request provider config and workflow inspection
All checks were successful
Build and Test / build (push) Successful in 21s
Release / build (push) Successful in 40s
Add support for per-request AI provider configuration and workflow YAML inspection endpoint, enabling multi-tenant AI operations.

Per-Request Provider Config:
- Add CreateConversation overload accepting provider, model, and API key
- Add CreateConversation helper method in AIController to resolve config cascade
- When ProviderConfig is provided in request, override sidecar defaults
- Falls back to sidecar configuration when not provided (backwards compatible)

This fixes the critical multi-tenant gap where all organizations shared the sidecar's hardcoded provider configuration.

Workflow Inspection:
- Add POST /api/v1/workflows/inspect endpoint
- Analyzes GitHub Actions/Gitea Actions YAML for issues
- Detects syntax errors, security issues, performance problems, and best practice violations
- Returns structured JSON with issues (line, severity, message, fix) and suggestions
- Supports runner label compatibility checking

Both features support the ProviderConfigDto pattern for tenant-specific AI configuration.
2026-02-13 01:15:47 -05:00
9 changed files with 528 additions and 17 deletions

View File

File diff suppressed because one or more lines are too long

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

@@ -58,6 +58,27 @@ public class AIProviderFactory : IAIProviderFactory
return builder;
}
public AIConversationBuilder CreateConversation(string provider, string model, string apiKey)
{
var aiProvider = ParseProvider(provider);
if (string.IsNullOrEmpty(apiKey))
{
// Fall back to configured key if the per-request key is empty
return CreateConversation(provider, model);
}
_logger.LogDebug("Creating conversation with per-request provider override: {Provider}, model {Model}", provider, model);
var builder = AIConversationBuilder.Create()
.UseProvider(aiProvider, apiKey)
.UseModel(model)
.WithMaxTokens(_serviceOptions.MaxTokens)
.WithTemperature(_serviceOptions.Temperature);
return builder;
}
private static AIProvider ParseProvider(string provider)
{
return provider.ToLowerInvariant() switch

View File

@@ -24,4 +24,10 @@ public interface IAIProviderFactory
/// Creates a new conversation builder with a specific provider and model.
/// </summary>
AIConversationBuilder CreateConversation(string provider, string model);
/// <summary>
/// Creates a new conversation builder with a specific provider, model, and API key.
/// Used for per-request provider overrides from the server's cascade config.
/// </summary>
AIConversationBuilder CreateConversation(string provider, string model, string apiKey);
}

View File

@@ -2,7 +2,9 @@
// SPDX-License-Identifier: BSL-1.1
using GitCaddy.AI.Service.Services;
using GitCaddy.AI.Service.Configuration;
using GitCaddy.AI.Service.Licensing;
using MarketAlly.AIPlugin.Conversation;
using Microsoft.AspNetCore.Mvc;
namespace GitCaddy.AI.Service.Controllers;
@@ -19,6 +21,7 @@ public class AIController : ControllerBase
private readonly ICodeIntelligenceService _codeIntelligenceService;
private readonly IIssueService _issueService;
private readonly IDocumentationService _documentationService;
private readonly IAIProviderFactory _providerFactory;
private readonly ILicenseValidator _licenseValidator;
private readonly ILogger<AIController> _logger;
@@ -27,6 +30,7 @@ public class AIController : ControllerBase
ICodeIntelligenceService codeIntelligenceService,
IIssueService issueService,
IDocumentationService documentationService,
IAIProviderFactory providerFactory,
ILicenseValidator licenseValidator,
ILogger<AIController> logger)
{
@@ -34,10 +38,26 @@ public class AIController : ControllerBase
_codeIntelligenceService = codeIntelligenceService;
_issueService = issueService;
_documentationService = documentationService;
_providerFactory = providerFactory;
_licenseValidator = licenseValidator;
_logger = logger;
}
/// <summary>
/// Creates a conversation builder using per-request provider config if available,
/// otherwise falls back to the sidecar's default configuration.
/// </summary>
private AIConversationBuilder CreateConversation(ProviderConfigDto? providerConfig)
{
if (providerConfig is not null &&
!string.IsNullOrEmpty(providerConfig.Provider))
{
var model = !string.IsNullOrEmpty(providerConfig.Model) ? providerConfig.Model : providerConfig.Provider;
return _providerFactory.CreateConversation(providerConfig.Provider, model, providerConfig.ApiKey ?? "");
}
return _providerFactory.CreateConversation();
}
/// <summary>
/// Health check endpoint
/// </summary>
@@ -388,6 +408,88 @@ public class AIController : ControllerBase
}
}
/// <summary>
/// Inspect a workflow YAML file for issues
/// </summary>
[HttpPost("workflows/inspect")]
public async Task<IActionResult> InspectWorkflow([FromBody] InspectWorkflowDto request, CancellationToken cancellationToken)
{
try
{
var conversation = CreateConversation(request.ProviderConfig)
.WithSystemPrompt("""
You are a CI/CD workflow expert. Analyze the provided GitHub Actions / Gitea Actions
workflow YAML file for issues including:
- Syntax errors and invalid YAML
- Missing required fields (runs-on, steps, etc.)
- Security issues (hardcoded secrets, overly broad permissions, script injection)
- Performance issues (unnecessary steps, missing caching)
- Compatibility issues with the available runner labels
- Best practice violations
Respond with a JSON object containing:
{
"valid": true/false,
"issues": [{"line": 0, "severity": "error|warning|info", "message": "...", "fix": "..."}],
"suggestions": ["..."]
}
Only respond with the JSON object, no other text.
""")
.Build();
var prompt = $"Workflow file: {request.FilePath}\n\n```yaml\n{request.Content}\n```";
if (request.RunnerLabels is { Count: > 0 })
{
prompt += $"\n\nAvailable runner labels: {string.Join(", ", request.RunnerLabels)}";
}
var aiResponse = await conversation.SendAsync(prompt, cancellationToken);
var responseText = aiResponse.FinalMessage ?? "{}";
// Parse the AI response as JSON
try
{
var result = System.Text.Json.JsonSerializer.Deserialize<InspectWorkflowResult>(
responseText,
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true });
return Ok(new
{
valid = result?.Valid ?? true,
issues = result?.Issues?.Select(i => new
{
line = i.Line,
severity = i.Severity,
message = i.Message,
fix = i.Fix
}).ToList() ?? [],
suggestions = result?.Suggestions ?? [],
confidence = 0.8,
input_tokens = 0,
output_tokens = 0
});
}
catch (System.Text.Json.JsonException)
{
// If AI didn't return valid JSON, wrap the text response
return Ok(new
{
valid = true,
issues = Array.Empty<object>(),
suggestions = new[] { responseText },
confidence = 0.5,
input_tokens = 0,
output_tokens = 0
});
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to inspect workflow");
return StatusCode(500, new { error = ex.Message });
}
}
private static object MapReviewResponse(Proto.ReviewPullRequestResponse response)
{
return new
@@ -425,8 +527,23 @@ public class AIController : ControllerBase
}
// DTO classes for REST API
/// <summary>
/// Per-request provider configuration override.
/// When provided, overrides the sidecar's default provider/model/key for this request.
/// </summary>
public class ProviderConfigDto
{
public string? Provider { get; set; }
public string? Model { get; set; }
[System.Text.Json.Serialization.JsonPropertyName("api_key")]
public string? ApiKey { get; set; }
}
public class ReviewPullRequestDto
{
[System.Text.Json.Serialization.JsonPropertyName("provider_config")]
public ProviderConfigDto? ProviderConfig { get; set; }
public long RepoId { get; set; }
public long PullRequestId { get; set; }
public string? BaseBranch { get; set; }
@@ -460,6 +577,8 @@ public class ReviewOptionsDto
public class TriageIssueDto
{
[System.Text.Json.Serialization.JsonPropertyName("provider_config")]
public ProviderConfigDto? ProviderConfig { get; set; }
public long RepoId { get; set; }
public long IssueId { get; set; }
public string? Title { get; set; }
@@ -470,6 +589,8 @@ public class TriageIssueDto
public class SuggestLabelsDto
{
[System.Text.Json.Serialization.JsonPropertyName("provider_config")]
public ProviderConfigDto? ProviderConfig { get; set; }
public long RepoId { get; set; }
public string? Title { get; set; }
public string? Body { get; set; }
@@ -478,6 +599,8 @@ public class SuggestLabelsDto
public class ExplainCodeDto
{
[System.Text.Json.Serialization.JsonPropertyName("provider_config")]
public ProviderConfigDto? ProviderConfig { get; set; }
public long RepoId { get; set; }
public string? FilePath { get; set; }
public string? Code { get; set; }
@@ -488,6 +611,8 @@ public class ExplainCodeDto
public class SummarizeChangesDto
{
[System.Text.Json.Serialization.JsonPropertyName("provider_config")]
public ProviderConfigDto? ProviderConfig { get; set; }
public long RepoId { get; set; }
public List<FileDiffDto>? Files { get; set; }
public string? Context { get; set; }
@@ -495,6 +620,8 @@ public class SummarizeChangesDto
public class GenerateDocumentationDto
{
[System.Text.Json.Serialization.JsonPropertyName("provider_config")]
public ProviderConfigDto? ProviderConfig { get; set; }
public long RepoId { get; set; }
public string? FilePath { get; set; }
public string? Code { get; set; }
@@ -505,6 +632,8 @@ public class GenerateDocumentationDto
public class GenerateCommitMessageDto
{
[System.Text.Json.Serialization.JsonPropertyName("provider_config")]
public ProviderConfigDto? ProviderConfig { get; set; }
public long RepoId { get; set; }
public List<FileDiffDto>? Files { get; set; }
public string? Style { get; set; }
@@ -512,6 +641,8 @@ public class GenerateCommitMessageDto
public class GenerateIssueResponseDto
{
[System.Text.Json.Serialization.JsonPropertyName("provider_config")]
public ProviderConfigDto? ProviderConfig { get; set; }
public long RepoId { get; set; }
public long IssueId { get; set; }
public string? Title { get; set; }
@@ -527,3 +658,32 @@ public class IssueCommentDto
public string? Body { get; set; }
public string? CreatedAt { get; set; }
}
public class InspectWorkflowDto
{
[System.Text.Json.Serialization.JsonPropertyName("provider_config")]
public ProviderConfigDto? ProviderConfig { get; set; }
[System.Text.Json.Serialization.JsonPropertyName("repo_id")]
public long RepoId { get; set; }
[System.Text.Json.Serialization.JsonPropertyName("file_path")]
public string? FilePath { get; set; }
public string? Content { get; set; }
[System.Text.Json.Serialization.JsonPropertyName("runner_labels")]
public List<string>? RunnerLabels { get; set; }
}
// Internal model for parsing AI response
internal class InspectWorkflowResult
{
public bool Valid { get; set; }
public List<InspectWorkflowIssue>? Issues { get; set; }
public List<string>? Suggestions { get; set; }
}
internal class InspectWorkflowIssue
{
public int Line { get; set; }
public string? Severity { get; set; }
public string? Message { get; set; }
public string? Fix { get; set; }
}

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;
}
}