M3: BYOK — encrypted owner-only API configs + model adapters
SharedKernel: Autonomy dial enum; IModelClient (ModelRequest/ModelCompletion);
IApiConfigResolver (+ ApiConfigSummary/ResolvedApiConfig) — server-side, decrypted.
Integrations module:
- ApiConfig entity (org-scoped) + IntegrationsDbContext (schema "integrations") +
InitialIntegrations migration; the key is AES-256-GCM encrypted at rest (key derived from
Encryption:MasterKey) and never returned to a client.
- Model adapters: StubModelClient (no-network, provider "stub"/"echo"), an OpenAI-compatible
HTTP adapter, and a ModelClientRouter; ApiConfigResolver decrypts server-side only.
- Endpoints: POST/GET/DELETE /api/integrations/api-configs and POST .../{id}/test. Create/
test/delete require ManageApiKeys (owner); listing requires ConfigureAgents (assign-only,
no key). Dev master key in appsettings; override Encryption__MasterKey in prod.
Verified: build green; ArchitectureTests 8/8 (Integrations references only SharedKernel);
IntegrationTests 26/26 incl. a BYOK flow — key never appears in any response, the connection
test succeeds (stub), and a Member is 403'd from create + list.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,17 +1,23 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using TeamUp.Modules.Integrations.Ai;
|
||||
using TeamUp.Modules.Integrations.Endpoints;
|
||||
using TeamUp.Modules.Integrations.Git;
|
||||
using TeamUp.Modules.Integrations.Persistence;
|
||||
using TeamUp.Modules.Integrations.Security;
|
||||
using TeamUp.SharedKernel.Ai;
|
||||
using TeamUp.SharedKernel.Git;
|
||||
using TeamUp.SharedKernel.Modularity;
|
||||
using TeamUp.SharedKernel.Persistence;
|
||||
|
||||
namespace TeamUp.Modules.Integrations;
|
||||
|
||||
/// <summary>
|
||||
/// BYOK API configs, the Git connection, the encrypted-credential store. In M2 it provides the
|
||||
/// <see cref="IGitProvider"/> (filesystem for dogfood, Gitea over REST). BYOK lands in M3.
|
||||
/// BYOK API configs (encrypted, owner-only), the model-client adapters, and the Git connection.
|
||||
/// Encryption keys are owner-only and server-side; the decrypted key never leaves the server.
|
||||
/// </summary>
|
||||
public sealed class IntegrationsModule : IModule
|
||||
{
|
||||
@@ -19,10 +25,26 @@ public sealed class IntegrationsModule : IModule
|
||||
|
||||
public void Register(IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.Configure<GitSourceOptions>(configuration.GetSection(GitSourceOptions.SectionName));
|
||||
var options = configuration.GetSection(GitSourceOptions.SectionName).Get<GitSourceOptions>() ?? new GitSourceOptions();
|
||||
var connectionString = configuration.GetConnectionString("Postgres")
|
||||
?? throw new InvalidOperationException("Missing connection string 'ConnectionStrings:Postgres'.");
|
||||
|
||||
if (string.Equals(options.Provider, "gitea", StringComparison.OrdinalIgnoreCase))
|
||||
// BYOK credential store + encryption.
|
||||
services.AddDbContext<IntegrationsDbContext>(options => options.UseNpgsql(connectionString));
|
||||
services.AddScoped<IModuleDbContext>(sp => sp.GetRequiredService<IntegrationsDbContext>());
|
||||
services.Configure<EncryptionOptions>(configuration.GetSection(EncryptionOptions.SectionName));
|
||||
services.AddSingleton<ISecretProtector, AesGcmSecretProtector>();
|
||||
services.AddScoped<IApiConfigResolver, ApiConfigResolver>();
|
||||
services.TryAddSingleton(TimeProvider.System);
|
||||
|
||||
// Model clients: a router over per-provider adapters.
|
||||
services.AddSingleton<StubModelClient>();
|
||||
services.AddHttpClient<OpenAiCompatibleModelClient>();
|
||||
services.AddScoped<IModelClient, ModelClientRouter>();
|
||||
|
||||
// Git source (M2) — filesystem for dogfood, Gitea over REST when configured.
|
||||
services.Configure<GitSourceOptions>(configuration.GetSection(GitSourceOptions.SectionName));
|
||||
var gitOptions = configuration.GetSection(GitSourceOptions.SectionName).Get<GitSourceOptions>() ?? new GitSourceOptions();
|
||||
if (string.Equals(gitOptions.Provider, "gitea", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
services.AddHttpClient<IGitProvider, GiteaGitProvider>();
|
||||
}
|
||||
@@ -32,10 +54,5 @@ public sealed class IntegrationsModule : IModule
|
||||
}
|
||||
}
|
||||
|
||||
public void MapEndpoints(IEndpointRouteBuilder endpoints)
|
||||
{
|
||||
endpoints.MapGroup($"/api/{Name}")
|
||||
.WithTags("Integrations")
|
||||
.MapGet("/ping", () => TypedResults.Ok(new ModulePing(Name)));
|
||||
}
|
||||
public void MapEndpoints(IEndpointRouteBuilder endpoints) => IntegrationsEndpoints.Map(endpoints);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user