Changeset 0.9.4 (#54)

This commit is contained in:
2026-02-24 10:44:12 +00:00
parent 90021157e6
commit 5e416d3726
26 changed files with 1333 additions and 108 deletions

View File

@@ -13,6 +13,7 @@ import (
"git.gobha.me/xcaliber/chat-switchboard/database"
"git.gobha.me/xcaliber/chat-switchboard/models"
"git.gobha.me/xcaliber/chat-switchboard/providers"
"git.gobha.me/xcaliber/chat-switchboard/crypto"
capspkg "git.gobha.me/xcaliber/chat-switchboard/capabilities"
"git.gobha.me/xcaliber/chat-switchboard/tools"
)
@@ -33,11 +34,13 @@ type completionRequest struct {
}
// CompletionHandler proxies LLM requests through the backend.
type CompletionHandler struct{}
type CompletionHandler struct{
vault *crypto.KeyResolver
}
// NewCompletionHandler creates a new handler.
func NewCompletionHandler() *CompletionHandler {
return &CompletionHandler{}
func NewCompletionHandler(vault *crypto.KeyResolver) *CompletionHandler {
return &CompletionHandler{vault: vault}
}
// ── Chat Completion ─────────────────────────
@@ -387,16 +390,20 @@ func (h *CompletionHandler) resolveConfig(userID string, channelID string, req c
// Load the config — allow personal, global, OR team configs the user belongs to
var providerID, endpoint string
var apiKey, modelDefault *string
var modelDefault *string
var apiKeyEnc, keyNonce []byte
var keyScope string
var customHeadersJSON, providerSettingsJSON []byte
err := database.DB.QueryRow(`
SELECT provider, endpoint, api_key_enc, model_default, headers, settings
SELECT provider, endpoint, api_key_enc, key_nonce, key_scope,
model_default, headers, settings
FROM provider_configs
WHERE id = $1 AND is_active = true
AND (scope = 'global'
OR (scope = 'personal' AND owner_id = $2)
OR (scope = 'team' AND owner_id IN (SELECT team_id FROM team_members WHERE user_id = $2)))
`, configID, userID).Scan(&providerID, &endpoint, &apiKey, &modelDefault, &customHeadersJSON, &providerSettingsJSON)
`, configID, userID).Scan(&providerID, &endpoint, &apiKeyEnc, &keyNonce, &keyScope,
&modelDefault, &customHeadersJSON, &providerSettingsJSON)
if err == sql.ErrNoRows {
return providers.ProviderConfig{}, "", "", "", fmt.Errorf("API config not found or not accessible")
@@ -414,9 +421,22 @@ func (h *CompletionHandler) resolveConfig(userID string, channelID string, req c
return providers.ProviderConfig{}, "", "", "", fmt.Errorf("no model specified and no default model in config")
}
// Decrypt API key using the appropriate tier
key := ""
if apiKey != nil {
key = *apiKey
if len(apiKeyEnc) > 0 {
if h.vault != nil {
var err error
key, err = h.vault.Decrypt(apiKeyEnc, keyNonce, keyScope, userID)
if err != nil {
if err == crypto.ErrVaultLocked {
return providers.ProviderConfig{}, "", "", "", fmt.Errorf("personal vault is locked — please log in again")
}
return providers.ProviderConfig{}, "", "", "", fmt.Errorf("failed to decrypt API key: %w", err)
}
} else {
// No vault — key stored as raw bytes (unencrypted fallback)
key = string(apiKeyEnc)
}
}
// Parse custom headers