Files
agent-radar 2ed3415fb7
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
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

698 lines
19 KiB
Go

package managedcredentials
import (
"context"
"crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"sync"
"time"
managedcredentialsbiz "github.com/tutti-os/tutti/services/tuttid/biz/managedcredentials"
workspacedata "github.com/tutti-os/tutti/services/tuttid/data/workspace"
"github.com/tutti-os/tutti/packages/agent/daemon/httpx"
)
const GrantCodeTTL = 5 * time.Hour
var (
ErrInvalidProvider = errors.New("invalid managed credential provider")
ErrGrantCodeInvalid = errors.New("managed credential grant code is invalid")
ErrGrantExpired = errors.New("managed credential grant is expired")
ErrGrantRevoked = errors.New("managed credential grant is revoked")
ErrProviderNotConfigured = errors.New("managed credential provider is not configured")
)
type Service struct {
Store workspacedata.ManagedCredentialsStore
Now func() time.Time
HTTPClient *http.Client
mu sync.Mutex
grantCodes map[string]grantCodeState
}
type grantCodeState struct {
ContextToken string
WorkspaceID string
AppID string
GrantRef string
Nonce string
State string
ExpiresAt time.Time
Used bool
}
type PutProviderInput struct {
WorkspaceID string
Provider string
Enabled bool
APIKey *string
BaseURL string
Models []managedcredentialsbiz.Model
}
type CreateGrantInput struct {
ContextToken string
WorkspaceID string
AppID string
Nonce string
ProviderIDs []string
Scopes []string
State string
}
type GrantResult struct {
GrantCode string
Grant managedcredentialsbiz.Grant
Models []managedcredentialsbiz.Model
}
type ExchangeInput struct {
ContextToken string
WorkspaceID string
AppID string
GrantCode string
Nonce string
State string
}
type ExchangeResult struct {
ExpiresAt time.Time
GrantRef string
Providers []managedcredentialsbiz.ProviderID
Models []managedcredentialsbiz.Model
}
type CredentialInput struct {
WorkspaceID string
AppID string
GrantRef string
Provider string
Model string
Capability string
}
type CredentialResult struct {
ExpiresAt time.Time
Credential managedcredentialsbiz.ProviderCredential
GrantModels []managedcredentialsbiz.Model
}
type ModelCatalogResult struct {
ExpiresAt time.Time
Models []managedcredentialsbiz.Model
}
type ListProviderModelsInput struct {
WorkspaceID string
Provider string
APIKey *string
BaseURL string
}
func (s *Service) ListProviders(ctx context.Context, workspaceID string) ([]managedcredentialsbiz.PublicProviderConfig, error) {
configs, err := s.Store.ListManagedModelProviderConfigs(ctx, strings.TrimSpace(workspaceID))
if err != nil {
return nil, err
}
public := make([]managedcredentialsbiz.PublicProviderConfig, 0, len(configs))
for _, config := range configs {
public = append(public, managedcredentialsbiz.PublicProvider(config))
}
return public, nil
}
func (s *Service) PutProvider(ctx context.Context, input PutProviderInput) (managedcredentialsbiz.PublicProviderConfig, error) {
provider, err := normalizeProvider(input.Provider)
if err != nil {
return managedcredentialsbiz.PublicProviderConfig{}, err
}
apiKey := ""
if input.APIKey != nil {
apiKey = strings.TrimSpace(*input.APIKey)
} else {
if existing, err := s.Store.GetManagedModelProviderConfig(ctx, strings.TrimSpace(input.WorkspaceID), provider); err == nil {
apiKey = existing.APIKey
}
}
config := managedcredentialsbiz.ProviderConfig{
WorkspaceID: strings.TrimSpace(input.WorkspaceID),
Provider: provider,
Enabled: input.Enabled,
APIKey: apiKey,
BaseURL: strings.TrimSpace(input.BaseURL),
Models: normalizeModels(provider, input.Models),
UpdatedAt: s.now(),
}
if err := s.Store.PutManagedModelProviderConfig(ctx, config); err != nil {
return managedcredentialsbiz.PublicProviderConfig{}, err
}
config, err = s.Store.GetManagedModelProviderConfig(ctx, config.WorkspaceID, config.Provider)
if err != nil {
return managedcredentialsbiz.PublicProviderConfig{}, err
}
return managedcredentialsbiz.PublicProvider(config), nil
}
func (s *Service) DeleteProvider(ctx context.Context, workspaceID string, providerID string) error {
provider, err := normalizeProvider(providerID)
if err != nil {
return err
}
return s.Store.DeleteManagedModelProviderConfig(ctx, strings.TrimSpace(workspaceID), provider)
}
func (s *Service) TestProvider(ctx context.Context, workspaceID string, providerID string) error {
provider, err := normalizeProvider(providerID)
if err != nil {
return err
}
config, err := s.Store.GetManagedModelProviderConfig(ctx, strings.TrimSpace(workspaceID), provider)
if err != nil {
return err
}
if !config.Enabled || config.APIKey == "" {
return ErrProviderNotConfigured
}
return nil
}
func (s *Service) ListProviderModels(ctx context.Context, input ListProviderModelsInput) (ModelCatalogResult, error) {
provider, err := normalizeProvider(input.Provider)
if err != nil {
return ModelCatalogResult{}, err
}
config := managedcredentialsbiz.ProviderConfig{
WorkspaceID: strings.TrimSpace(input.WorkspaceID),
Provider: provider,
APIKey: strings.TrimSpace(derefString(input.APIKey)),
BaseURL: strings.TrimSpace(input.BaseURL),
}
if config.APIKey == "" || config.BaseURL == "" {
saved, err := s.Store.GetManagedModelProviderConfig(ctx, strings.TrimSpace(input.WorkspaceID), provider)
if err != nil {
return ModelCatalogResult{}, err
}
if config.APIKey == "" {
config.APIKey = saved.APIKey
}
if config.BaseURL == "" {
config.BaseURL = saved.BaseURL
}
}
if config.APIKey == "" {
return ModelCatalogResult{}, ErrProviderNotConfigured
}
models, err := s.fetchProviderModels(ctx, config)
if err != nil {
return ModelCatalogResult{}, err
}
return ModelCatalogResult{
Models: normalizeModels(provider, models),
}, nil
}
func (s *Service) CreateGrant(ctx context.Context, input CreateGrantInput) (GrantResult, error) {
workspaceID := strings.TrimSpace(input.WorkspaceID)
appID := strings.TrimSpace(input.AppID)
if strings.TrimSpace(input.State) == "" ||
strings.TrimSpace(input.Nonce) == "" ||
strings.TrimSpace(input.ContextToken) == "" {
return GrantResult{}, ErrGrantCodeInvalid
}
providers, err := normalizeProviders(input.ProviderIDs)
if err != nil {
return GrantResult{}, err
}
if len(providers) == 0 {
configs, err := s.Store.ListManagedModelProviderConfigs(ctx, workspaceID)
if err != nil {
return GrantResult{}, err
}
for _, config := range configs {
if config.Enabled && config.APIKey != "" {
providers = append(providers, config.Provider)
}
}
}
now := s.now()
grantRef := randomToken()
grant := managedcredentialsbiz.Grant{
WorkspaceID: workspaceID,
AppID: appID,
GrantRef: grantRef,
ProviderIDs: providers,
Scopes: normalizeScopes(input.Scopes),
CreatedAt: now,
ExpiresAt: now.Add(GrantCodeTTL),
}
if err := s.Store.PutManagedModelGrant(ctx, grant); err != nil {
return GrantResult{}, err
}
code := randomToken()
s.mu.Lock()
s.ensure()
s.grantCodes[code] = grantCodeState{
ContextToken: strings.TrimSpace(input.ContextToken),
WorkspaceID: workspaceID,
AppID: appID,
GrantRef: grantRef,
Nonce: strings.TrimSpace(input.Nonce),
State: strings.TrimSpace(input.State),
ExpiresAt: grant.ExpiresAt,
}
s.mu.Unlock()
return GrantResult{
GrantCode: code,
Grant: grant,
Models: s.modelsForProviders(ctx, workspaceID, providers),
}, nil
}
func (s *Service) Exchange(ctx context.Context, input ExchangeInput) (ExchangeResult, error) {
workspaceID := strings.TrimSpace(input.WorkspaceID)
appID := strings.TrimSpace(input.AppID)
code := strings.TrimSpace(input.GrantCode)
if code == "" {
return ExchangeResult{}, ErrGrantCodeInvalid
}
state, err := s.consumeGrantCode(code, grantCodeState{
ContextToken: strings.TrimSpace(input.ContextToken),
WorkspaceID: workspaceID,
AppID: appID,
Nonce: strings.TrimSpace(input.Nonce),
State: strings.TrimSpace(input.State),
}, s.now())
if err != nil {
return ExchangeResult{}, err
}
grant, err := s.readActiveGrant(ctx, workspaceID, appID, state.GrantRef)
if err != nil {
return ExchangeResult{}, err
}
return ExchangeResult{
ExpiresAt: grant.ExpiresAt,
GrantRef: grant.GrantRef,
Providers: append([]managedcredentialsbiz.ProviderID(nil), grant.ProviderIDs...),
Models: s.modelsForProviders(ctx, workspaceID, grant.ProviderIDs),
}, nil
}
func (s *Service) ListGrantModels(ctx context.Context, workspaceID string, appID string, grantRef string) (ModelCatalogResult, error) {
grant, err := s.readActiveGrant(ctx, strings.TrimSpace(workspaceID), strings.TrimSpace(appID), strings.TrimSpace(grantRef))
if err != nil {
return ModelCatalogResult{}, err
}
return ModelCatalogResult{
ExpiresAt: grant.ExpiresAt,
Models: s.modelsForProviders(ctx, grant.WorkspaceID, grant.ProviderIDs),
}, nil
}
func (s *Service) Credential(ctx context.Context, input CredentialInput) (CredentialResult, error) {
workspaceID := strings.TrimSpace(input.WorkspaceID)
appID := strings.TrimSpace(input.AppID)
grantRef := strings.TrimSpace(input.GrantRef)
grant, err := s.readActiveGrant(ctx, workspaceID, appID, grantRef)
if err != nil {
return CredentialResult{}, err
}
provider, err := normalizeProvider(input.Provider)
if err != nil {
return CredentialResult{}, err
}
allowed := false
for _, grantProvider := range grant.ProviderIDs {
if grantProvider == provider {
allowed = true
break
}
}
if !allowed {
return CredentialResult{}, ErrProviderNotConfigured
}
config, err := s.Store.GetManagedModelProviderConfig(ctx, workspaceID, provider)
if err != nil {
return CredentialResult{}, err
}
if !config.Enabled || config.APIKey == "" {
return CredentialResult{}, ErrProviderNotConfigured
}
model := strings.TrimSpace(input.Model)
if model != "" && !modelsContain(config.Models, model) {
return CredentialResult{}, ErrProviderNotConfigured
}
return CredentialResult{
ExpiresAt: grant.ExpiresAt,
Credential: managedcredentialsbiz.ProviderCredential{
Provider: config.Provider,
APIKey: config.APIKey,
BaseURL: config.BaseURL,
Models: config.Models,
},
GrantModels: config.Models,
}, nil
}
func (s *Service) readActiveGrant(ctx context.Context, workspaceID string, appID string, grantRef string) (managedcredentialsbiz.Grant, error) {
grant, err := s.Store.GetManagedModelGrant(ctx, workspaceID, appID, grantRef)
if err != nil {
return managedcredentialsbiz.Grant{}, err
}
if grant.RevokedAt != nil {
return managedcredentialsbiz.Grant{}, ErrGrantRevoked
}
if !grant.ExpiresAt.After(s.now()) {
return managedcredentialsbiz.Grant{}, ErrGrantExpired
}
return grant, nil
}
func (s *Service) Revoke(ctx context.Context, workspaceID string, appID string, grantRef string) error {
return s.Store.RevokeManagedModelGrant(ctx, strings.TrimSpace(workspaceID), strings.TrimSpace(appID), strings.TrimSpace(grantRef))
}
func (s *Service) now() time.Time {
if s.Now != nil {
return s.Now().UTC()
}
return time.Now().UTC()
}
func (s *Service) httpClient() *http.Client {
if s.HTTPClient != nil {
return s.HTTPClient
}
return httpx.NewClient(10 * time.Second)
}
func (s *Service) fetchProviderModels(ctx context.Context, config managedcredentialsbiz.ProviderConfig) ([]managedcredentialsbiz.Model, error) {
catalogURLs, err := providerModelCatalogURLs(config.Provider, config.BaseURL)
if err != nil {
return nil, err
}
var lastStatus string
for _, catalogURL := range catalogURLs {
request, err := http.NewRequestWithContext(ctx, http.MethodGet, catalogURL, nil)
if err != nil {
return nil, err
}
switch config.Provider {
case managedcredentialsbiz.ProviderAnthropic:
request.Header.Set("x-api-key", config.APIKey)
request.Header.Set("anthropic-version", "2023-06-01")
default:
request.Header.Set("Authorization", "Bearer "+config.APIKey)
}
request.Header.Set("Accept", "application/json")
response, err := s.httpClient().Do(request)
if err != nil {
return nil, err
}
if response.StatusCode == http.StatusNotFound || response.StatusCode == http.StatusMethodNotAllowed {
lastStatus = response.Status
response.Body.Close()
continue
}
if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices {
response.Body.Close()
return nil, fmt.Errorf("managed credential provider model catalog returned %s", response.Status)
}
var payload providerModelsResponse
if err := json.NewDecoder(io.LimitReader(response.Body, 1<<20)).Decode(&payload); err != nil {
response.Body.Close()
return nil, fmt.Errorf("decode managed credential provider model catalog: %w", err)
}
response.Body.Close()
return payload.models(config.Provider), nil
}
if lastStatus == "" {
lastStatus = "no candidate endpoints"
}
return nil, fmt.Errorf("managed credential provider model catalog candidates failed: %s", lastStatus)
}
type providerModelsResponse struct {
Data []providerModelItem `json:"data"`
Models []providerModelItem `json:"models"`
}
type providerModelItem struct {
DisplayName string `json:"display_name"`
ID string `json:"id"`
Name string `json:"name"`
}
func (r providerModelsResponse) models(provider managedcredentialsbiz.ProviderID) []managedcredentialsbiz.Model {
items := r.Data
if len(items) == 0 {
items = r.Models
}
models := make([]managedcredentialsbiz.Model, 0, len(items))
for _, item := range items {
id := strings.TrimSpace(item.ID)
if id == "" {
continue
}
name := strings.TrimSpace(item.Name)
if name == "" {
name = strings.TrimSpace(item.DisplayName)
}
models = append(models, managedcredentialsbiz.Model{
ID: id,
Name: name,
Provider: provider,
})
}
return models
}
func providerModelCatalogURLs(provider managedcredentialsbiz.ProviderID, baseURL string) ([]string, error) {
trimmedBaseURL := strings.TrimSpace(baseURL)
if trimmedBaseURL == "" {
trimmedBaseURL = defaultProviderBaseURL(provider)
}
parsed, err := url.Parse(trimmedBaseURL)
if err != nil {
return nil, err
}
parsed.RawQuery = ""
parsed.Fragment = ""
parsed.Path = strings.TrimRight(parsed.Path, "/")
base := parsed.String()
var candidates []string
if endsWithVersionSegment(parsed.Path) {
candidates = append(candidates, base+"/models")
if !strings.HasSuffix(parsed.Path, "/v1") {
candidates = append(candidates, base+"/v1/models")
}
} else {
candidates = append(candidates, base+"/v1/models")
}
if stripped, ok := stripKnownModelCatalogSuffix(parsed); ok {
candidates = append(candidates, stripped+"/v1/models", stripped+"/models")
}
return uniqueStrings(candidates), nil
}
func defaultProviderBaseURL(provider managedcredentialsbiz.ProviderID) string {
switch provider {
case managedcredentialsbiz.ProviderAgnes:
return "https://apihub.agnes-ai.com/v1"
case managedcredentialsbiz.ProviderAnthropic:
return "https://api.anthropic.com/v1"
default:
return "https://api.openai.com/v1"
}
}
func derefString(value *string) string {
if value == nil {
return ""
}
return *value
}
func endsWithVersionSegment(path string) bool {
last := path[strings.LastIndex(path, "/")+1:]
if len(last) < 2 || last[0] != 'v' {
return false
}
for _, char := range last[1:] {
if char < '0' || char > '9' {
return false
}
}
return true
}
func stripKnownModelCatalogSuffix(parsed *url.URL) (string, bool) {
suffixes := []string{
"/api/claudecode",
"/api/anthropic",
"/apps/anthropic",
"/api/coding",
"/claudecode",
"/anthropic",
"/step_plan",
"/coding",
"/claude",
}
path := strings.TrimRight(parsed.Path, "/")
for _, suffix := range suffixes {
if strings.HasSuffix(path, suffix) {
copy := *parsed
copy.Path = strings.TrimRight(path[:len(path)-len(suffix)], "/")
copy.RawQuery = ""
copy.Fragment = ""
return copy.String(), true
}
}
return "", false
}
func uniqueStrings(values []string) []string {
unique := make([]string, 0, len(values))
for _, value := range values {
seen := false
for _, existing := range unique {
if existing == value {
seen = true
break
}
}
if !seen {
unique = append(unique, value)
}
}
return unique
}
func (s *Service) ensure() {
if s.grantCodes == nil {
s.grantCodes = map[string]grantCodeState{}
}
}
func (s *Service) consumeGrantCode(code string, expected grantCodeState, now time.Time) (grantCodeState, error) {
s.mu.Lock()
defer s.mu.Unlock()
s.ensure()
state, ok := s.grantCodes[code]
if !ok ||
state.Used ||
state.WorkspaceID != expected.WorkspaceID ||
state.AppID != expected.AppID ||
state.State != expected.State ||
state.Nonce != expected.Nonce ||
state.ContextToken != expected.ContextToken {
return grantCodeState{}, ErrGrantCodeInvalid
}
if !state.ExpiresAt.After(now) {
delete(s.grantCodes, code)
return grantCodeState{}, ErrGrantCodeInvalid
}
state.Used = true
s.grantCodes[code] = state
return state, nil
}
func (s *Service) modelsForProviders(ctx context.Context, workspaceID string, providers []managedcredentialsbiz.ProviderID) []managedcredentialsbiz.Model {
var models []managedcredentialsbiz.Model
for _, provider := range providers {
config, err := s.Store.GetManagedModelProviderConfig(ctx, workspaceID, provider)
if err == nil && config.Enabled && config.APIKey != "" {
models = append(models, config.Models...)
}
}
return models
}
func modelsContain(models []managedcredentialsbiz.Model, modelID string) bool {
for _, model := range models {
if strings.TrimSpace(model.ID) == modelID {
return true
}
}
return false
}
func normalizeProvider(value string) (managedcredentialsbiz.ProviderID, error) {
value = strings.TrimSpace(value)
if value == "openai-compatible" {
value = string(managedcredentialsbiz.ProviderOpenAI)
}
if !managedcredentialsbiz.IsProviderID(value) {
return "", ErrInvalidProvider
}
return managedcredentialsbiz.ProviderID(value), nil
}
func normalizeProviders(values []string) ([]managedcredentialsbiz.ProviderID, error) {
seen := map[managedcredentialsbiz.ProviderID]bool{}
var providers []managedcredentialsbiz.ProviderID
for _, value := range values {
provider, err := normalizeProvider(value)
if err != nil {
return nil, err
}
if seen[provider] {
continue
}
seen[provider] = true
providers = append(providers, provider)
}
return providers, nil
}
func normalizeModels(provider managedcredentialsbiz.ProviderID, models []managedcredentialsbiz.Model) []managedcredentialsbiz.Model {
seen := map[string]bool{}
var normalized []managedcredentialsbiz.Model
for _, model := range models {
id := strings.TrimSpace(model.ID)
if id == "" || seen[id] {
continue
}
seen[id] = true
name := strings.TrimSpace(model.Name)
if name == "" {
name = id
}
normalized = append(normalized, managedcredentialsbiz.Model{
ID: id,
Name: name,
Provider: provider,
})
}
return normalized
}
func normalizeScopes(values []string) []string {
seen := map[string]bool{}
var scopes []string
for _, value := range values {
scope := strings.TrimSpace(value)
if scope == "" || seen[scope] {
continue
}
seen[scope] = true
scopes = append(scopes, scope)
}
return scopes
}
func randomToken() string {
buf := make([]byte, 32)
_, _ = rand.Read(buf)
return base64.RawURLEncoding.EncodeToString(buf)
}