Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
288 lines
9.8 KiB
Go
288 lines
9.8 KiB
Go
package workspace
|
|
|
|
import (
|
|
"context"
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"database/sql"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
managedcredentialsbiz "github.com/tutti-os/tutti/services/tuttid/biz/managedcredentials"
|
|
)
|
|
|
|
func (s *SQLiteStore) ListManagedModelProviderConfigs(ctx context.Context, workspaceID string) ([]managedcredentialsbiz.ProviderConfig, error) {
|
|
if s == nil || s.writeDB == nil {
|
|
return nil, errors.New("workspace database is not initialized")
|
|
}
|
|
rows, err := s.readDB.QueryContext(ctx, `
|
|
SELECT workspace_id, provider_id, enabled, api_key_ciphertext, base_url, models_json, updated_at_unix_ms
|
|
FROM managed_model_provider_credentials
|
|
WHERE workspace_id = ?
|
|
ORDER BY provider_id
|
|
`, workspaceID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list managed model provider configs: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var configs []managedcredentialsbiz.ProviderConfig
|
|
for rows.Next() {
|
|
config, err := scanManagedModelProviderConfig(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
configs = append(configs, config)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("list managed model provider config rows: %w", err)
|
|
}
|
|
return configs, nil
|
|
}
|
|
|
|
func (s *SQLiteStore) GetManagedModelProviderConfig(ctx context.Context, workspaceID string, providerID managedcredentialsbiz.ProviderID) (managedcredentialsbiz.ProviderConfig, error) {
|
|
if s == nil || s.writeDB == nil {
|
|
return managedcredentialsbiz.ProviderConfig{}, errors.New("workspace database is not initialized")
|
|
}
|
|
row := s.readDB.QueryRowContext(ctx, `
|
|
SELECT workspace_id, provider_id, enabled, api_key_ciphertext, base_url, models_json, updated_at_unix_ms
|
|
FROM managed_model_provider_credentials
|
|
WHERE workspace_id = ? AND provider_id = ?
|
|
`, workspaceID, providerID)
|
|
config, err := scanManagedModelProviderConfig(row)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return managedcredentialsbiz.ProviderConfig{}, ErrWorkspaceAppNotFound
|
|
}
|
|
return managedcredentialsbiz.ProviderConfig{}, err
|
|
}
|
|
return config, nil
|
|
}
|
|
|
|
func (s *SQLiteStore) PutManagedModelProviderConfig(ctx context.Context, config managedcredentialsbiz.ProviderConfig) error {
|
|
if s == nil || s.writeDB == nil {
|
|
return errors.New("workspace database is not initialized")
|
|
}
|
|
modelsJSON, err := json.Marshal(config.Models)
|
|
if err != nil {
|
|
return fmt.Errorf("marshal managed provider models: %w", err)
|
|
}
|
|
ciphertext, err := encryptManagedCredential(config.APIKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
now := unixMs(time.Now().UTC())
|
|
_, err = s.writeDB.ExecContext(ctx, `
|
|
INSERT INTO managed_model_provider_credentials (
|
|
workspace_id, provider_id, enabled, api_key_ciphertext, base_url, models_json, updated_at_unix_ms
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
ON CONFLICT(workspace_id, provider_id) DO UPDATE SET
|
|
enabled = excluded.enabled,
|
|
api_key_ciphertext = excluded.api_key_ciphertext,
|
|
base_url = excluded.base_url,
|
|
models_json = excluded.models_json,
|
|
updated_at_unix_ms = excluded.updated_at_unix_ms
|
|
`, config.WorkspaceID, string(config.Provider), boolInt(config.Enabled), ciphertext, config.BaseURL, string(modelsJSON), now)
|
|
if err != nil {
|
|
return fmt.Errorf("put managed model provider config: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *SQLiteStore) DeleteManagedModelProviderConfig(ctx context.Context, workspaceID string, providerID managedcredentialsbiz.ProviderID) error {
|
|
if s == nil || s.writeDB == nil {
|
|
return errors.New("workspace database is not initialized")
|
|
}
|
|
_, err := s.writeDB.ExecContext(ctx, `
|
|
DELETE FROM managed_model_provider_credentials
|
|
WHERE workspace_id = ? AND provider_id = ?
|
|
`, workspaceID, providerID)
|
|
if err != nil {
|
|
return fmt.Errorf("delete managed model provider config: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *SQLiteStore) PutManagedModelGrant(ctx context.Context, grant managedcredentialsbiz.Grant) error {
|
|
if s == nil || s.writeDB == nil {
|
|
return errors.New("workspace database is not initialized")
|
|
}
|
|
providersJSON, err := json.Marshal(grant.ProviderIDs)
|
|
if err != nil {
|
|
return fmt.Errorf("marshal managed grant providers: %w", err)
|
|
}
|
|
scopesJSON, err := json.Marshal(grant.Scopes)
|
|
if err != nil {
|
|
return fmt.Errorf("marshal managed grant scopes: %w", err)
|
|
}
|
|
_, err = s.writeDB.ExecContext(ctx, `
|
|
INSERT INTO managed_model_app_grants (
|
|
workspace_id, app_id, grant_ref, provider_ids_json, scopes_json, created_at_unix_ms, expires_at_unix_ms, revoked_at_unix_ms
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, NULL)
|
|
ON CONFLICT(workspace_id, app_id, grant_ref) DO UPDATE SET
|
|
provider_ids_json = excluded.provider_ids_json,
|
|
scopes_json = excluded.scopes_json,
|
|
expires_at_unix_ms = excluded.expires_at_unix_ms,
|
|
revoked_at_unix_ms = NULL
|
|
`, grant.WorkspaceID, grant.AppID, grant.GrantRef, string(providersJSON), string(scopesJSON), unixMs(grant.CreatedAt), unixMs(grant.ExpiresAt))
|
|
if err != nil {
|
|
return fmt.Errorf("put managed model grant: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *SQLiteStore) GetManagedModelGrant(ctx context.Context, workspaceID string, appID string, grantRef string) (managedcredentialsbiz.Grant, error) {
|
|
if s == nil || s.writeDB == nil {
|
|
return managedcredentialsbiz.Grant{}, errors.New("workspace database is not initialized")
|
|
}
|
|
row := s.readDB.QueryRowContext(ctx, `
|
|
SELECT workspace_id, app_id, grant_ref, provider_ids_json, scopes_json, created_at_unix_ms, expires_at_unix_ms, revoked_at_unix_ms
|
|
FROM managed_model_app_grants
|
|
WHERE workspace_id = ? AND app_id = ? AND grant_ref = ?
|
|
`, workspaceID, appID, grantRef)
|
|
var grant managedcredentialsbiz.Grant
|
|
var providersJSON string
|
|
var scopesJSON string
|
|
var createdAtUnixMS int64
|
|
var expiresAtUnixMS int64
|
|
var revokedAt sql.NullInt64
|
|
if err := row.Scan(&grant.WorkspaceID, &grant.AppID, &grant.GrantRef, &providersJSON, &scopesJSON, &createdAtUnixMS, &expiresAtUnixMS, &revokedAt); err != nil {
|
|
return managedcredentialsbiz.Grant{}, err
|
|
}
|
|
_ = json.Unmarshal([]byte(providersJSON), &grant.ProviderIDs)
|
|
_ = json.Unmarshal([]byte(scopesJSON), &grant.Scopes)
|
|
grant.CreatedAt = time.UnixMilli(createdAtUnixMS).UTC()
|
|
grant.ExpiresAt = time.UnixMilli(expiresAtUnixMS).UTC()
|
|
grant.RevokedAt = nullableUnixMs(revokedAt)
|
|
return grant, nil
|
|
}
|
|
|
|
func (s *SQLiteStore) RevokeManagedModelGrant(ctx context.Context, workspaceID string, appID string, grantRef string) error {
|
|
if s == nil || s.writeDB == nil {
|
|
return errors.New("workspace database is not initialized")
|
|
}
|
|
_, err := s.writeDB.ExecContext(ctx, `
|
|
UPDATE managed_model_app_grants
|
|
SET revoked_at_unix_ms = ?
|
|
WHERE workspace_id = ? AND app_id = ? AND grant_ref = ?
|
|
`, unixMs(time.Now().UTC()), workspaceID, appID, grantRef)
|
|
if err != nil {
|
|
return fmt.Errorf("revoke managed model grant: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *SQLiteStore) DeleteManagedModelGrant(ctx context.Context, workspaceID string, appID string, grantRef string) error {
|
|
if s == nil || s.writeDB == nil {
|
|
return errors.New("workspace database is not initialized")
|
|
}
|
|
_, err := s.writeDB.ExecContext(ctx, `
|
|
DELETE FROM managed_model_app_grants
|
|
WHERE workspace_id = ? AND app_id = ? AND grant_ref = ?
|
|
`, workspaceID, appID, grantRef)
|
|
if err != nil {
|
|
return fmt.Errorf("delete managed model grant: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type managedProviderScanner interface {
|
|
Scan(dest ...any) error
|
|
}
|
|
|
|
func scanManagedModelProviderConfig(row managedProviderScanner) (managedcredentialsbiz.ProviderConfig, error) {
|
|
var config managedcredentialsbiz.ProviderConfig
|
|
var providerID string
|
|
var enabled int
|
|
var ciphertext string
|
|
var modelsJSON string
|
|
var updatedAtUnixMS int64
|
|
if err := row.Scan(&config.WorkspaceID, &providerID, &enabled, &ciphertext, &config.BaseURL, &modelsJSON, &updatedAtUnixMS); err != nil {
|
|
return managedcredentialsbiz.ProviderConfig{}, err
|
|
}
|
|
apiKey, err := decryptManagedCredential(ciphertext)
|
|
if err != nil {
|
|
return managedcredentialsbiz.ProviderConfig{}, err
|
|
}
|
|
config.Provider = managedcredentialsbiz.ProviderID(providerID)
|
|
config.Enabled = enabled != 0
|
|
config.APIKey = apiKey
|
|
_ = json.Unmarshal([]byte(modelsJSON), &config.Models)
|
|
config.UpdatedAt = time.UnixMilli(updatedAtUnixMS).UTC()
|
|
return config, nil
|
|
}
|
|
|
|
func boolInt(value bool) int {
|
|
if value {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func managedCredentialKey() []byte {
|
|
seed := strings.TrimSpace(os.Getenv("TUTTI_MANAGED_CREDENTIAL_SECRET"))
|
|
if seed == "" {
|
|
hostname, _ := os.Hostname()
|
|
userConfigDir, _ := os.UserConfigDir()
|
|
seed = "tutti-managed-credentials:" + hostname + ":" + userConfigDir
|
|
}
|
|
sum := sha256.Sum256([]byte(seed))
|
|
return sum[:]
|
|
}
|
|
|
|
func encryptManagedCredential(value string) (string, error) {
|
|
if value == "" {
|
|
return "", nil
|
|
}
|
|
block, err := aes.NewCipher(managedCredentialKey())
|
|
if err != nil {
|
|
return "", fmt.Errorf("create managed credential cipher: %w", err)
|
|
}
|
|
gcm, err := cipher.NewGCM(block)
|
|
if err != nil {
|
|
return "", fmt.Errorf("create managed credential gcm: %w", err)
|
|
}
|
|
nonce := make([]byte, gcm.NonceSize())
|
|
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
|
return "", fmt.Errorf("create managed credential nonce: %w", err)
|
|
}
|
|
ciphertext := gcm.Seal(nonce, nonce, []byte(value), nil)
|
|
return base64.StdEncoding.EncodeToString(ciphertext), nil
|
|
}
|
|
|
|
func decryptManagedCredential(value string) (string, error) {
|
|
if value == "" {
|
|
return "", nil
|
|
}
|
|
raw, err := base64.StdEncoding.DecodeString(value)
|
|
if err != nil {
|
|
return "", fmt.Errorf("decode managed credential: %w", err)
|
|
}
|
|
block, err := aes.NewCipher(managedCredentialKey())
|
|
if err != nil {
|
|
return "", fmt.Errorf("create managed credential cipher: %w", err)
|
|
}
|
|
gcm, err := cipher.NewGCM(block)
|
|
if err != nil {
|
|
return "", fmt.Errorf("create managed credential gcm: %w", err)
|
|
}
|
|
if len(raw) < gcm.NonceSize() {
|
|
return "", errors.New("managed credential ciphertext is malformed")
|
|
}
|
|
nonce := raw[:gcm.NonceSize()]
|
|
ciphertext := raw[gcm.NonceSize():]
|
|
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
|
|
if err != nil {
|
|
return "", fmt.Errorf("decrypt managed credential: %w", err)
|
|
}
|
|
return string(plaintext), nil
|
|
}
|