From 682c58b930871fac18c942a769d2394fb70bdea1 Mon Sep 17 00:00:00 2001 From: Donnie Adams Date: Wed, 20 May 2026 19:20:05 -0400 Subject: [PATCH 1/2] chore: migrate away from GPTScript for credentials This change moves credentials into Obot itself, migrating any existing credentials from GPTScript. Signed-off-by: Donnie Adams --- aws-encryption.yaml | 2 +- azure-encryption.yaml | 2 +- chart/templates/deployment.yaml | 2 +- gcp-encryption.yaml | 2 +- go.mod | 2 +- pkg/api/handlers/auditlogexport.go | 14 +- pkg/api/handlers/authprovider.go | 42 ++-- pkg/api/handlers/availablemodels.go | 16 +- pkg/api/handlers/imagepullsecrets.go | 35 ++-- pkg/api/handlers/mcp.go | 160 +++++++------- pkg/api/handlers/mcp_oauth_debugger.go | 10 +- pkg/api/handlers/mcpcatalogs.go | 90 ++++---- pkg/api/handlers/mcpgateway/handler.go | 14 +- .../mcpgateway/oauth/mcpoauthhandler.go | 19 +- pkg/api/handlers/mcphelpers.go | 30 ++- pkg/api/handlers/mcpwebhookvalidation.go | 66 +++--- pkg/api/handlers/modelprovider.go | 48 ++--- pkg/api/handlers/poweruserworkspace.go | 22 +- pkg/api/handlers/registry/handler.go | 14 +- pkg/api/handlers/serverinstances.go | 22 +- pkg/api/handlers/systemmcpcatalogs.go | 22 +- pkg/api/handlers/systemmcpserver.go | 56 ++--- pkg/api/request.go | 2 - pkg/api/router/router.go | 4 +- pkg/api/server/server.go | 6 +- pkg/auditlogexport/credentials.go | 69 +++--- pkg/auditlogexport/storage.go | 7 +- pkg/bootstrap/bootstrap.go | 30 ++- pkg/controller/controller.go | 4 +- .../handlers/auditlogexport/auditlogexport.go | 9 +- .../handlers/cleanup/credentials.go | 24 +-- .../imagepullsecret/imagepullsecret.go | 16 +- .../handlers/mcpcatalog/mcpcatalog.go | 11 +- .../handlers/mcpserver/mcpserver.go | 28 +-- .../mcpservercatalogentry.go | 36 ++-- .../mcpwebhookvalidation.go | 36 ++-- .../handlers/nanobotagent/nanobotagent.go | 31 ++- .../handlers/oauthclients/oauthclients.go | 17 +- .../handlers/secret/nanobotagent.go | 18 +- .../systemmcpserver/systemmcpserver.go | 60 +++--- .../handlers/toolreference/toolreference.go | 81 ++++++-- pkg/controller/migrate.go | 20 +- pkg/controller/routes.go | 22 +- pkg/credstores/credstore.go | 27 ++- pkg/gateway/client/credential.go | 196 ++++++++++++++++++ pkg/gateway/client/credential_migration.go | 131 ++++++++++++ pkg/gateway/db/db.go | 1 + .../server/dispatcher/availablemodels.go | 11 +- pkg/gateway/server/dispatcher/dispatcher.go | 70 +++---- pkg/gateway/server/llmproxy.go | 4 +- pkg/gateway/server/tokenreview.go | 11 +- pkg/gateway/server/user.go | 4 +- pkg/gateway/types/credentials.go | 14 ++ pkg/invoke/invoker.go | 16 +- pkg/invoke/system.go | 5 +- pkg/jwt/persistent/persistent.go | 50 ++--- pkg/messagepolicy/evaluate.go | 4 +- pkg/messagepolicy/helper.go | 20 +- pkg/proxy/proxy.go | 13 +- pkg/services/config.go | 85 +++++--- pkg/tools/resolve.go | 44 ---- 61 files changed, 1110 insertions(+), 817 deletions(-) create mode 100644 pkg/gateway/client/credential.go create mode 100644 pkg/gateway/client/credential_migration.go create mode 100644 pkg/gateway/types/credentials.go delete mode 100644 pkg/tools/resolve.go diff --git a/aws-encryption.yaml b/aws-encryption.yaml index 1c767e241c..51c9b57cfe 100644 --- a/aws-encryption.yaml +++ b/aws-encryption.yaml @@ -2,7 +2,7 @@ apiVersion: apiserver.config.k8s.io/v1 kind: EncryptionConfiguration resources: - resources: - - credentials + - credentials.obot.obot.ai - runstates.obot.obot.ai - users.obot.obot.ai - identities.obot.obot.ai diff --git a/azure-encryption.yaml b/azure-encryption.yaml index 326f2cf70f..aca7ac1fd4 100644 --- a/azure-encryption.yaml +++ b/azure-encryption.yaml @@ -2,7 +2,7 @@ apiVersion: apiserver.config.k8s.io/v1 kind: EncryptionConfiguration resources: - resources: - - credentials + - credentials.obot.obot.ai - runstates.obot.obot.ai - users.obot.obot.ai - identities.obot.obot.ai diff --git a/chart/templates/deployment.yaml b/chart/templates/deployment.yaml index 0416840b1c..4eb5220014 100644 --- a/chart/templates/deployment.yaml +++ b/chart/templates/deployment.yaml @@ -56,7 +56,7 @@ spec: apiVersion: apiserver.config.k8s.io/v1 resources: - resources: - - credentials + - credentials.obot.obot.ai - runstates.obot.obot.ai - users.obot.obot.ai - identities.obot.obot.ai diff --git a/gcp-encryption.yaml b/gcp-encryption.yaml index 3ab5dbc446..040add73d9 100644 --- a/gcp-encryption.yaml +++ b/gcp-encryption.yaml @@ -2,7 +2,7 @@ apiVersion: apiserver.config.k8s.io/v1 kind: EncryptionConfiguration resources: - resources: - - credentials + - credentials.obot.obot.ai - runstates.obot.obot.ai - users.obot.obot.ai - identities.obot.obot.ai diff --git a/go.mod b/go.mod index 70711e738a..8e2fa80494 100644 --- a/go.mod +++ b/go.mod @@ -31,6 +31,7 @@ require ( github.com/go-git/go-git/v5 v5.17.0 github.com/gobwas/glob v0.2.3 github.com/golang-jwt/jwt/v5 v5.3.0 + github.com/glebarez/sqlite v1.11.0 github.com/google/jsonschema-go v0.4.2 github.com/google/uuid v1.6.0 github.com/gptscript-ai/chat-completion-client v0.0.0-20250224164718-139cb4507b1d @@ -185,7 +186,6 @@ require ( github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/getkin/kin-openapi v0.132.0 // indirect github.com/glebarez/go-sqlite v1.22.0 // indirect - github.com/glebarez/sqlite v1.11.0 // indirect github.com/go-errors/errors v1.4.2 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-gorp/gorp/v3 v3.1.0 // indirect diff --git a/pkg/api/handlers/auditlogexport.go b/pkg/api/handlers/auditlogexport.go index 152be43488..4d594f2741 100644 --- a/pkg/api/handlers/auditlogexport.go +++ b/pkg/api/handlers/auditlogexport.go @@ -4,10 +4,10 @@ import ( "errors" "fmt" - "github.com/gptscript-ai/go-gptscript" "github.com/obot-platform/obot/apiclient/types" "github.com/obot-platform/obot/pkg/api" "github.com/obot-platform/obot/pkg/auditlogexport" + gateway "github.com/obot-platform/obot/pkg/gateway/client" v1 "github.com/obot-platform/obot/pkg/storage/apis/obot.obot.ai/v1" "github.com/obot-platform/obot/pkg/system" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -15,12 +15,12 @@ import ( ) type AuditLogExportHandler struct { - credProvider *auditlogexport.GPTScriptCredentialProvider + credProvider *auditlogexport.CredentialProvider } -func NewAuditLogExportHandler(gptClient *gptscript.GPTScript) *AuditLogExportHandler { +func NewAuditLogExportHandler(gatewayClient *gateway.Client) *AuditLogExportHandler { return &AuditLogExportHandler{ - credProvider: auditlogexport.NewGPTScriptCredentialProvider(gptClient), + credProvider: auditlogexport.NewCredentialProvider(gatewayClient), } } @@ -282,9 +282,9 @@ func (h *AuditLogExportHandler) ConfigureStorageCredentials(req api.Context) err // GetStorageCredentials gets the storage provider credentials func (h *AuditLogExportHandler) GetStorageCredentials(req api.Context) error { storageConfig, err := h.credProvider.GetStorageConfig(req.Context()) - if err != nil && !errors.As(err, &gptscript.ErrNotFound{}) { + if err != nil && !errors.As(err, &gateway.CredentialNotFoundError{}) { return fmt.Errorf("failed to get storage credentials: %w", err) - } else if errors.As(err, &gptscript.ErrNotFound{}) { + } else if errors.As(err, &gateway.CredentialNotFoundError{}) { return types.NewErrNotFound("storage credentials not found") } @@ -347,7 +347,7 @@ func (h *AuditLogExportHandler) TestStorageCredentials(req api.Context) error { // when using test credentials, if user doesn't provide secret, use the existing secret var existingStorageConfig types.StorageConfig storageConfig, err := h.credProvider.GetStorageConfig(req.Context()) - if err != nil && !errors.As(err, &gptscript.ErrNotFound{}) { + if err != nil && !errors.As(err, &gateway.CredentialNotFoundError{}) { return fmt.Errorf("failed to get storage credentials: %w", err) } else if err == nil { existingStorageConfig = *storageConfig diff --git a/pkg/api/handlers/authprovider.go b/pkg/api/handlers/authprovider.go index b306768b6b..c868b51fd7 100644 --- a/pkg/api/handlers/authprovider.go +++ b/pkg/api/handlers/authprovider.go @@ -7,11 +7,12 @@ import ( "errors" "fmt" - "github.com/gptscript-ai/go-gptscript" "github.com/obot-platform/obot/apiclient/types" "github.com/obot-platform/obot/pkg/api" "github.com/obot-platform/obot/pkg/api/handlers/providers" + gateway "github.com/obot-platform/obot/pkg/gateway/client" "github.com/obot-platform/obot/pkg/gateway/server/dispatcher" + gatewaytypes "github.com/obot-platform/obot/pkg/gateway/types" v1 "github.com/obot-platform/obot/pkg/storage/apis/obot.obot.ai/v1" "github.com/obot-platform/obot/pkg/system" "github.com/tidwall/gjson" @@ -54,11 +55,11 @@ func (ap *AuthProviderHandler) ByID(req api.Context) error { return err } if len(aps.RequiredConfigurationParameters) > 0 { - cred, err := req.GPTClient.RevealCredential(req.Context(), []string{string(ref.UID), system.GenericAuthProviderCredentialContext}, ref.Name) - if err != nil && !errors.As(err, &gptscript.ErrNotFound{}) { + cred, err := req.GatewayClient.RevealCredential(req.Context(), []string{string(ref.UID), system.GenericAuthProviderCredentialContext}, ref.Name) + if err != nil && !errors.As(err, &gateway.CredentialNotFoundError{}) { return fmt.Errorf("failed to reveal credential for auth provider %q: %w", ref.Name, err) } else if err == nil { - credEnvVars = cred.Env + credEnvVars = cred.Secrets } } } @@ -97,7 +98,7 @@ func (ap *AuthProviderHandler) listAuthProviders(req api.Context) ([]types.AuthP } credCtxs = append(credCtxs, system.GenericAuthProviderCredentialContext) - creds, err := req.GPTClient.ListCredentials(req.Context(), gptscript.ListCredentialsOptions{ + creds, err := req.GatewayClient.ListCredentials(req.Context(), gateway.ListCredentialsOptions{ CredentialContexts: credCtxs, }) if err != nil { @@ -106,7 +107,7 @@ func (ap *AuthProviderHandler) listAuthProviders(req api.Context) ([]types.AuthP credMap := make(map[string]map[string]string, len(creds)) for _, cred := range creds { - credMap[cred.Context+cred.ToolName] = cred.Env + credMap[cred.Context+cred.Name] = cred.Secrets } resp := make([]types.AuthProvider, 0, len(refList.Items)) @@ -157,12 +158,12 @@ func (ap *AuthProviderHandler) Configure(req api.Context) error { envVars[providers.CookieSecretEnvVar] = cookieSecret // Allow for updating credentials. The only way to update a credential is to delete the existing one and recreate it. - cred, err := req.GPTClient.RevealCredential(req.Context(), []string{string(ref.UID), system.GenericAuthProviderCredentialContext}, ref.Name) + cred, err := req.GatewayClient.RevealCredential(req.Context(), []string{string(ref.UID), system.GenericAuthProviderCredentialContext}, ref.Name) if err != nil { - if !errors.As(err, &gptscript.ErrNotFound{}) { + if !errors.As(err, &gateway.CredentialNotFoundError{}) { return fmt.Errorf("failed to find credential: %w", err) } - } else if err = req.GPTClient.DeleteCredential(req.Context(), cred.Context, ref.Name); err != nil { + } else if _, err = req.GatewayClient.DeleteCredential(req.Context(), cred.Context, ref.Name); err != nil { return fmt.Errorf("failed to remove existing credential: %w", err) } @@ -172,11 +173,10 @@ func (ap *AuthProviderHandler) Configure(req api.Context) error { } } - if err := req.GPTClient.CreateCredential(req.Context(), gptscript.Credential{ - Context: string(ref.UID), - ToolName: ref.Name, - Type: gptscript.CredentialTypeTool, - Env: envVars, + if err := req.GatewayClient.UpsertCredential(req.Context(), gatewaytypes.Credential{ + Context: string(ref.UID), + Name: ref.Name, + Secrets: envVars, }); err != nil { return fmt.Errorf("failed to create credential for auth provider %q: %w", ref.Name, err) } @@ -191,7 +191,7 @@ func (ap *AuthProviderHandler) Configure(req api.Context) error { } if configuredProvider != "" && configuredProvider != ref.Name { // Delete the credential we just configured - _ = req.GPTClient.DeleteCredential(req.Context(), string(ref.UID), ref.Name) + _, _ = req.GatewayClient.DeleteCredential(req.Context(), string(ref.UID), ref.Name) return types.NewErrBadRequest( "only one authentication provider can be configured at a time. Please deconfigure %q first", configuredProvider, @@ -219,12 +219,12 @@ func (ap *AuthProviderHandler) Deconfigure(req api.Context) error { return types.NewErrBadRequest("%q is not an auth provider", ref.Name) } - cred, err := req.GPTClient.RevealCredential(req.Context(), []string{string(ref.UID), system.GenericAuthProviderCredentialContext}, ref.Name) + cred, err := req.GatewayClient.RevealCredential(req.Context(), []string{string(ref.UID), system.GenericAuthProviderCredentialContext}, ref.Name) if err != nil { - if !errors.As(err, &gptscript.ErrNotFound{}) { + if !errors.As(err, &gateway.CredentialNotFoundError{}) { return fmt.Errorf("failed to find credential: %w", err) } - } else if err = req.GPTClient.DeleteCredential(req.Context(), cred.Context, ref.Name); err != nil { + } else if _, err = req.GatewayClient.DeleteCredential(req.Context(), cred.Context, ref.Name); err != nil { return fmt.Errorf("failed to remove existing credential: %w", err) } @@ -280,11 +280,11 @@ func (ap *AuthProviderHandler) Reveal(req api.Context) error { return types.NewErrBadRequest("%q is not an auth provider", ref.Name) } - cred, err := req.GPTClient.RevealCredential(req.Context(), []string{string(ref.UID), system.GenericAuthProviderCredentialContext}, ref.Name) - if err != nil && !errors.As(err, &gptscript.ErrNotFound{}) { + cred, err := req.GatewayClient.RevealCredential(req.Context(), []string{string(ref.UID), system.GenericAuthProviderCredentialContext}, ref.Name) + if err != nil && !errors.As(err, &gateway.CredentialNotFoundError{}) { return fmt.Errorf("failed to reveal credential for auth provider %q: %w", ref.Name, err) } else if err == nil { - return req.Write(cred.Env) + return req.Write(cred.Secrets) } return types.NewErrNotFound("no credential found for %q", ref.Name) diff --git a/pkg/api/handlers/availablemodels.go b/pkg/api/handlers/availablemodels.go index 1d01021cf6..9f8875aab0 100644 --- a/pkg/api/handlers/availablemodels.go +++ b/pkg/api/handlers/availablemodels.go @@ -8,9 +8,9 @@ import ( "github.com/obot-platform/obot/pkg/api/handlers/providers" openai "github.com/gptscript-ai/chat-completion-client" - "github.com/gptscript-ai/go-gptscript" "github.com/obot-platform/obot/apiclient/types" "github.com/obot-platform/obot/pkg/api" + gateway "github.com/obot-platform/obot/pkg/gateway/client" "github.com/obot-platform/obot/pkg/gateway/server/dispatcher" v1 "github.com/obot-platform/obot/pkg/storage/apis/obot.obot.ai/v1" "github.com/obot-platform/obot/pkg/system" @@ -44,7 +44,7 @@ func (a *AvailableModelsHandler) List(req api.Context) error { credCtxs = append(credCtxs, string(ref.UID)) } - creds, err := req.GPTClient.ListCredentials(req.Context(), gptscript.ListCredentialsOptions{ + creds, err := req.GatewayClient.ListCredentials(req.Context(), gateway.ListCredentialsOptions{ CredentialContexts: credCtxs, }) if err != nil { @@ -53,7 +53,7 @@ func (a *AvailableModelsHandler) List(req api.Context) error { credMap := make(map[string]map[string]string, len(creds)) for _, cred := range creds { - credMap[cred.Context+cred.ToolName] = cred.Env + credMap[cred.Context+cred.Name] = cred.Secrets } var oModels openai.ModelsList @@ -67,7 +67,7 @@ func (a *AvailableModelsHandler) List(req api.Context) error { continue } - m, err := a.dispatcher.ModelsForProvider(req.Context(), req.GPTClient, modelProvider.Namespace, modelProvider.Name) + m, err := a.dispatcher.ModelsForProvider(req.Context(), modelProvider.Namespace, modelProvider.Name) if err != nil { return err } @@ -104,11 +104,11 @@ func (a *AvailableModelsHandler) ListForModelProvider(req api.Context) error { var credEnvVars map[string]string if modelProviderReference.Status.Tool != nil { if len(modelProvider.RequiredConfigurationParameters) > 0 { - cred, err := req.GPTClient.RevealCredential(req.Context(), credCtxs, modelProviderReference.Name) - if err != nil && !errors.As(err, &gptscript.ErrNotFound{}) { + cred, err := req.GatewayClient.RevealCredential(req.Context(), credCtxs, modelProviderReference.Name) + if err != nil && !errors.As(err, &gateway.CredentialNotFoundError{}) { return fmt.Errorf("failed to reveal credential for model provider %q: %w", modelProviderReference.Name, err) } else if err == nil { - credEnvVars = cred.Env + credEnvVars = cred.Secrets } } } @@ -121,7 +121,7 @@ func (a *AvailableModelsHandler) ListForModelProvider(req api.Context) error { return types.NewErrBadRequest("model provider %s is not configured, missing configuration parameters: %s", modelProviderReference.Name, strings.Join(modelProvider.MissingConfigurationParameters, ", ")) } - oModels, err := a.dispatcher.ModelsForProvider(req.Context(), req.GPTClient, modelProviderReference.Namespace, modelProviderReference.Name) + oModels, err := a.dispatcher.ModelsForProvider(req.Context(), modelProviderReference.Namespace, modelProviderReference.Name) if err != nil { return err } diff --git a/pkg/api/handlers/imagepullsecrets.go b/pkg/api/handlers/imagepullsecrets.go index 32bbccd1cc..4f7b6fd6fc 100644 --- a/pkg/api/handlers/imagepullsecrets.go +++ b/pkg/api/handlers/imagepullsecrets.go @@ -8,9 +8,10 @@ import ( "strings" "time" - "github.com/gptscript-ai/go-gptscript" "github.com/obot-platform/obot/apiclient/types" "github.com/obot-platform/obot/pkg/api" + gateway "github.com/obot-platform/obot/pkg/gateway/client" + gatewaytypes "github.com/obot-platform/obot/pkg/gateway/types" "github.com/obot-platform/obot/pkg/imagepullsecrets" "github.com/obot-platform/obot/pkg/mcp" v1 "github.com/obot-platform/obot/pkg/storage/apis/obot.obot.ai/v1" @@ -470,15 +471,15 @@ func listPasswordConfigured(req api.Context, secrets []v1.ImagePullSecret) (map[ return result, nil } - credentials, err := req.GPTClient.ListCredentials(req.Context(), gptscript.ListCredentialsOptions{ + credentials, err := req.GatewayClient.ListCredentials(req.Context(), gateway.ListCredentialsOptions{ CredentialContexts: []string{imagepullsecrets.CredentialContext}, }) if err != nil { return nil, err } for _, credential := range credentials { - if _, ok := result[credential.ToolName]; ok { - _, result[credential.ToolName] = credential.Env[imagepullsecrets.PasswordEnvVar] + if _, ok := result[credential.Name]; ok { + _, result[credential.Name] = credential.Secrets[imagepullsecrets.PasswordEnvVar] } } return result, nil @@ -494,25 +495,24 @@ func setPasswordConfigured(req api.Context, name string, status *types.ImagePull } func storeImagePullSecretPassword(req api.Context, name, password string) error { - return req.GPTClient.CreateCredential(req.Context(), gptscript.Credential{ - Type: gptscript.CredentialTypeTool, - Context: imagepullsecrets.CredentialContext, - ToolName: name, - Env: map[string]string{ + return req.GatewayClient.UpsertCredential(req.Context(), gatewaytypes.Credential{ + Context: imagepullsecrets.CredentialContext, + Name: name, + Secrets: map[string]string{ imagepullsecrets.PasswordEnvVar: password, }, }) } func revealImagePullSecretPassword(req api.Context, name string) (string, error) { - credential, err := req.GPTClient.RevealCredential(req.Context(), []string{imagepullsecrets.CredentialContext}, name) - if errors.As(err, &gptscript.ErrNotFound{}) { + credential, err := req.GatewayClient.RevealCredential(req.Context(), []string{imagepullsecrets.CredentialContext}, name) + if errors.As(err, &gateway.CredentialNotFoundError{}) { return "", types.NewErrBadRequest("password is not configured") } if err != nil { return "", err } - password := credential.Env[imagepullsecrets.PasswordEnvVar] + password := credential.Secrets[imagepullsecrets.PasswordEnvVar] if password == "" { return "", types.NewErrBadRequest("password is not configured") } @@ -520,21 +520,18 @@ func revealImagePullSecretPassword(req api.Context, name string) (string, error) } func passwordConfigured(req api.Context, name string) (bool, error) { - credential, err := req.GPTClient.RevealCredential(req.Context(), []string{imagepullsecrets.CredentialContext}, name) - if errors.As(err, &gptscript.ErrNotFound{}) { + credential, err := req.GatewayClient.RevealCredential(req.Context(), []string{imagepullsecrets.CredentialContext}, name) + if errors.As(err, &gateway.CredentialNotFoundError{}) { return false, nil } if err != nil { return false, err } - return credential.Env[imagepullsecrets.PasswordEnvVar] != "", nil + return credential.Secrets[imagepullsecrets.PasswordEnvVar] != "", nil } func deleteImagePullSecretPassword(req api.Context, name string) error { - err := req.GPTClient.DeleteCredential(req.Context(), imagepullsecrets.CredentialContext, name) - if errors.As(err, &gptscript.ErrNotFound{}) { - return nil - } + _, err := req.GatewayClient.DeleteCredential(req.Context(), imagepullsecrets.CredentialContext, name) return err } diff --git a/pkg/api/handlers/mcp.go b/pkg/api/handlers/mcp.go index 7ba6d8d117..0ec18501b5 100644 --- a/pkg/api/handlers/mcp.go +++ b/pkg/api/handlers/mcp.go @@ -13,12 +13,13 @@ import ( "golang.org/x/sync/errgroup" - "github.com/gptscript-ai/go-gptscript" "github.com/gptscript-ai/gptscript/pkg/hash" nmcp "github.com/obot-platform/nanobot/pkg/mcp" "github.com/obot-platform/obot/apiclient/types" "github.com/obot-platform/obot/pkg/accesscontrolrule" "github.com/obot-platform/obot/pkg/api" + gateway "github.com/obot-platform/obot/pkg/gateway/client" + gatewaytypes "github.com/obot-platform/obot/pkg/gateway/types" "github.com/obot-platform/obot/pkg/mcp" v1 "github.com/obot-platform/obot/pkg/storage/apis/obot.obot.ai/v1" "github.com/obot-platform/obot/pkg/system" @@ -248,7 +249,7 @@ func (m *MCPHandler) ListServer(req api.Context) error { } } - creds, err := req.GPTClient.ListCredentials(req.Context(), gptscript.ListCredentialsOptions{ + creds, err := req.GatewayClient.ListCredentials(req.Context(), gateway.ListCredentialsOptions{ CredentialContexts: credCtxs, }) if err != nil { @@ -257,12 +258,12 @@ func (m *MCPHandler) ListServer(req api.Context) error { credMap := make(map[string]map[string]string, len(creds)) for _, cred := range creds { - if _, ok := credMap[cred.ToolName]; !ok { - c, err := req.GPTClient.RevealCredential(req.Context(), []string{cred.Context}, cred.ToolName) - if err != nil && !errors.As(err, &gptscript.ErrNotFound{}) { + if _, ok := credMap[cred.Name]; !ok { + c, err := req.GatewayClient.RevealCredential(req.Context(), []string{cred.Context}, cred.Name) + if err != nil && !errors.As(err, &gateway.CredentialNotFoundError{}) { return fmt.Errorf("failed to find credential: %w", err) } - credMap[cred.ToolName] = c.Env + credMap[cred.Name] = c.Secrets } } @@ -364,12 +365,12 @@ func (m *MCPHandler) GetServer(req api.Context) error { credCtxs = []string{fmt.Sprintf("%s-%s", req.User.GetUID(), server.Name)} } - cred, err := req.GPTClient.RevealCredential(req.Context(), credCtxs, server.Name) - if err != nil && !errors.As(err, &gptscript.ErrNotFound{}) { + cred, err := req.GatewayClient.RevealCredential(req.Context(), credCtxs, server.Name) + if err != nil && !errors.As(err, &gateway.CredentialNotFoundError{}) { return fmt.Errorf("failed to find credential: %w", err) } - mergedEnv, err := mcp.MergeBoundCreds(req.Context(), req.LocalK8sClient, req.ObotNamespace, server.Spec.Manifest.Env, server.Spec.Manifest.RemoteConfig, cred.Env) + mergedEnv, err := mcp.MergeBoundCreds(req.Context(), req.LocalK8sClient, req.ObotNamespace, server.Spec.Manifest.Env, server.Spec.Manifest.RemoteConfig, cred.Secrets) if err != nil { return fmt.Errorf("failed to resolve secret bindings: %w", err) } @@ -1103,8 +1104,8 @@ func serverFromMCPServerInstance(req api.Context, instance v1.MCPServerInstance) scope = instance.Spec.UserID } - cred, err := req.GPTClient.RevealCredential(req.Context(), []string{credCtx}, server.Name) - if err != nil && !errors.As(err, &gptscript.ErrNotFound{}) { + cred, err := req.GatewayClient.RevealCredential(req.Context(), []string{credCtx}, server.Name) + if err != nil && !errors.As(err, &gateway.CredentialNotFoundError{}) { return server, mcp.ServerConfig{}, fmt.Errorf("failed to find credential: %w", err) } @@ -1130,18 +1131,18 @@ func serverFromMCPServerInstance(req api.Context, instance v1.MCPServerInstance) } } - tokenExchangeCred, err := req.GPTClient.RevealCredential(req.Context(), []string{server.Name}, server.Name) + tokenExchangeCred, err := req.GatewayClient.RevealCredential(req.Context(), []string{server.Name}, server.Name) if err != nil { return server, mcp.ServerConfig{}, fmt.Errorf("failed to find token exchange credential: %w", err) } - mergedEnv, err := mcp.MergeBoundCreds(req.Context(), req.LocalK8sClient, req.ObotNamespace, server.Spec.Manifest.Env, server.Spec.Manifest.RemoteConfig, cred.Env) + mergedEnv, err := mcp.MergeBoundCreds(req.Context(), req.LocalK8sClient, req.ObotNamespace, server.Spec.Manifest.Env, server.Spec.Manifest.RemoteConfig, cred.Secrets) if err != nil { return server, mcp.ServerConfig{}, fmt.Errorf("failed to resolve secret bindings: %w", err) } baseURL := strings.TrimSuffix(req.APIBaseURL, "/api") - serverConfig, missingConfig, err := mcp.ServerToServerConfig(server, instance.ValidConnectURLs(baseURL), baseURL, req.User.GetUID(), scope, catalogName, mergedEnv, tokenExchangeCred.Env) + serverConfig, missingConfig, err := mcp.ServerToServerConfig(server, instance.ValidConnectURLs(baseURL), baseURL, req.User.GetUID(), scope, catalogName, mergedEnv, tokenExchangeCred.Secrets) if err != nil { return server, mcp.ServerConfig{}, err } @@ -1208,12 +1209,12 @@ func serverConfigForAction(req api.Context, server v1.MCPServer) (mcp.ServerConf // Add extracted env vars to the server definition addExtractedEnvVars(&server) - cred, err := req.GPTClient.RevealCredential(req.Context(), credCtxs, server.Name) - if err != nil && !errors.As(err, &gptscript.ErrNotFound{}) { + cred, err := req.GatewayClient.RevealCredential(req.Context(), credCtxs, server.Name) + if err != nil && !errors.As(err, &gateway.CredentialNotFoundError{}) { return mcp.ServerConfig{}, fmt.Errorf("failed to find credential: %w", err) } - mergedEnv, err := mcp.MergeBoundCreds(req.Context(), req.LocalK8sClient, req.ObotNamespace, server.Spec.Manifest.Env, server.Spec.Manifest.RemoteConfig, cred.Env) + mergedEnv, err := mcp.MergeBoundCreds(req.Context(), req.LocalK8sClient, req.ObotNamespace, server.Spec.Manifest.Env, server.Spec.Manifest.RemoteConfig, cred.Secrets) if err != nil { return mcp.ServerConfig{}, fmt.Errorf("failed to resolve secret bindings: %w", err) } @@ -1250,7 +1251,7 @@ func serverConfigForAction(req api.Context, server v1.MCPServer) (mcp.ServerConf } var ( - tokenExchangeCred gptscript.Credential + tokenExchangeCred gatewaytypes.Credential tokenCredErr error ) if err = retry.OnError(kwait.Backoff{ @@ -1259,9 +1260,9 @@ func serverConfigForAction(req api.Context, server v1.MCPServer) (mcp.ServerConf Factor: 2.0, Jitter: 0.1, }, func(err error) bool { - return errors.As(err, &gptscript.ErrNotFound{}) + return errors.As(err, &gateway.CredentialNotFoundError{}) }, func() error { - tokenExchangeCred, tokenCredErr = req.GPTClient.RevealCredential(req.Context(), []string{server.Name}, server.Name) + tokenExchangeCred, tokenCredErr = req.GatewayClient.RevealCredential(req.Context(), []string{server.Name}, server.Name) return tokenCredErr }); err != nil { return mcp.ServerConfig{}, fmt.Errorf("failed to find token exchange credential: %w", tokenCredErr) @@ -1289,9 +1290,9 @@ func serverConfigForAction(req api.Context, server v1.MCPServer) (mcp.ServerConf return mcp.ServerConfig{}, fmt.Errorf("failed to list component servers instances: %w", err) } - serverConfig, missingConfig, err = mcp.CompositeServerToServerConfig(server, componentServers.Items, componentInstances.Items, server.ValidConnectURLs(baseURL), baseURL, req.User.GetUID(), scope, catalogName, mergedEnv, tokenExchangeCred.Env) + serverConfig, missingConfig, err = mcp.CompositeServerToServerConfig(server, componentServers.Items, componentInstances.Items, server.ValidConnectURLs(baseURL), baseURL, req.User.GetUID(), scope, catalogName, mergedEnv, tokenExchangeCred.Secrets) } else { - serverConfig, missingConfig, err = mcp.ServerToServerConfig(server, server.ValidConnectURLs(baseURL), baseURL, req.User.GetUID(), scope, catalogName, mergedEnv, tokenExchangeCred.Env) + serverConfig, missingConfig, err = mcp.ServerToServerConfig(server, server.ValidConnectURLs(baseURL), baseURL, req.User.GetUID(), scope, catalogName, mergedEnv, tokenExchangeCred.Secrets) } if err != nil { return mcp.ServerConfig{}, err @@ -1630,21 +1631,21 @@ func (m *MCPHandler) CreateServer(req api.Context) error { } var ( - cred gptscript.Credential + cred gatewaytypes.Credential err error ) if catalogID != "" { - cred, err = req.GPTClient.RevealCredential(req.Context(), []string{fmt.Sprintf("%s-%s", catalogID, server.Name)}, server.Name) + cred, err = req.GatewayClient.RevealCredential(req.Context(), []string{fmt.Sprintf("%s-%s", catalogID, server.Name)}, server.Name) } else if workspaceID != "" { - cred, err = req.GPTClient.RevealCredential(req.Context(), []string{fmt.Sprintf("%s-%s", workspaceID, server.Name)}, server.Name) + cred, err = req.GatewayClient.RevealCredential(req.Context(), []string{fmt.Sprintf("%s-%s", workspaceID, server.Name)}, server.Name) } else { - cred, err = req.GPTClient.RevealCredential(req.Context(), []string{fmt.Sprintf("%s-%s", req.User.GetUID(), server.Name)}, server.Name) + cred, err = req.GatewayClient.RevealCredential(req.Context(), []string{fmt.Sprintf("%s-%s", req.User.GetUID(), server.Name)}, server.Name) } - if err != nil && !errors.As(err, &gptscript.ErrNotFound{}) { + if err != nil && !errors.As(err, &gateway.CredentialNotFoundError{}) { return fmt.Errorf("failed to find credential: %w", err) } - mergedEnv, err := mcp.MergeBoundCreds(req.Context(), req.LocalK8sClient, req.ObotNamespace, server.Spec.Manifest.Env, server.Spec.Manifest.RemoteConfig, cred.Env) + mergedEnv, err := mcp.MergeBoundCreds(req.Context(), req.LocalK8sClient, req.ObotNamespace, server.Spec.Manifest.Env, server.Spec.Manifest.RemoteConfig, cred.Secrets) if err != nil { return fmt.Errorf("failed to resolve secret bindings: %w", err) } @@ -1688,15 +1689,15 @@ func (m *MCPHandler) UpdateServer(req api.Context) error { } // Shutdown any server that is using the default credentials. - var cred gptscript.Credential + var cred gatewaytypes.Credential if catalogID != "" { - cred, err = req.GPTClient.RevealCredential(req.Context(), []string{fmt.Sprintf("%s-%s", catalogID, existing.Name)}, existing.Name) + cred, err = req.GatewayClient.RevealCredential(req.Context(), []string{fmt.Sprintf("%s-%s", catalogID, existing.Name)}, existing.Name) } else if workspaceID != "" { - cred, err = req.GPTClient.RevealCredential(req.Context(), []string{fmt.Sprintf("%s-%s", workspaceID, existing.Name)}, existing.Name) + cred, err = req.GatewayClient.RevealCredential(req.Context(), []string{fmt.Sprintf("%s-%s", workspaceID, existing.Name)}, existing.Name) } else { - cred, err = req.GPTClient.RevealCredential(req.Context(), []string{fmt.Sprintf("%s-%s", req.User.GetUID(), existing.Name)}, existing.Name) + cred, err = req.GatewayClient.RevealCredential(req.Context(), []string{fmt.Sprintf("%s-%s", req.User.GetUID(), existing.Name)}, existing.Name) } - if err != nil && !errors.As(err, &gptscript.ErrNotFound{}) { + if err != nil && !errors.As(err, &gateway.CredentialNotFoundError{}) { return fmt.Errorf("failed to find credential: %w", err) } @@ -1756,7 +1757,7 @@ func (m *MCPHandler) UpdateServer(req api.Context) error { return fmt.Errorf("failed to generate slug: %w", err) } - mergedEnv, err := mcp.MergeBoundCreds(req.Context(), req.LocalK8sClient, req.ObotNamespace, existing.Spec.Manifest.Env, existing.Spec.Manifest.RemoteConfig, cred.Env) + mergedEnv, err := mcp.MergeBoundCreds(req.Context(), req.LocalK8sClient, req.ObotNamespace, existing.Spec.Manifest.Env, existing.Spec.Manifest.RemoteConfig, cred.Secrets) if err != nil { return fmt.Errorf("failed to resolve secret bindings: %w", err) } @@ -1879,17 +1880,16 @@ func (m *MCPHandler) ConfigureServer(req api.Context) error { } // Allow for updating credentials. The only way to update a credential is to delete the existing one and recreate it. - if err := m.removeMCPServerAndCred(req.Context(), req.GPTClient, mcpServer, []string{credCtx}); err != nil { + if err := m.removeMCPServerAndCred(req.Context(), req.GatewayClient, mcpServer, []string{credCtx}); err != nil { return err } sanitizeConfig(envVars, mcpServer.Spec.Manifest) - if err := req.GPTClient.CreateCredential(req.Context(), gptscript.Credential{ - Context: credCtx, - ToolName: mcpServer.Name, - Type: gptscript.CredentialTypeTool, - Env: envVars, + if err := req.GatewayClient.UpsertCredential(req.Context(), gatewaytypes.Credential{ + Context: credCtx, + Name: mcpServer.Name, + Secrets: envVars, }); err != nil { return fmt.Errorf("failed to create credential: %w", err) } @@ -1968,7 +1968,7 @@ func (m *MCPHandler) configureCompositeServer(req api.Context, compositeServer v var ( manifestChanged bool oldManifestHash = hash.Digest(compositeServer.Spec.Manifest) - componentCreds = make(map[string]gptscript.Credential, len(existingServers)) + componentCreds = make(map[string]gatewaytypes.Credential, len(existingServers)) componentInstance = make(map[string]v1.MCPServerInstance, len(existingInstances)) ) for i, component := range compositeServer.Spec.Manifest.CompositeConfig.ComponentServers { @@ -1991,11 +1991,10 @@ func (m *MCPHandler) configureCompositeServer(req api.Context, compositeServer v } if instance, instanceExists := existingInstances[componentID]; instanceExists && !component.Disabled { - componentCreds[componentID] = gptscript.Credential{ - Context: MCPServerInstanceCredentialContext(instance), - ToolName: instance.Name, - Type: gptscript.CredentialTypeTool, - Env: config.Config, + componentCreds[componentID] = gatewaytypes.Credential{ + Context: MCPServerInstanceCredentialContext(instance), + Name: instance.Name, + Secrets: config.Config, } componentInstance[componentID] = instance } @@ -2027,11 +2026,10 @@ func (m *MCPHandler) configureCompositeServer(req api.Context, compositeServer v } // Capture the updated credential - componentCreds[componentID] = gptscript.Credential{ - Context: fmt.Sprintf("%s-%s", req.User.GetUID(), server.Name), - ToolName: server.Name, - Type: gptscript.CredentialTypeTool, - Env: config.Config, + componentCreds[componentID] = gatewaytypes.Credential{ + Context: fmt.Sprintf("%s-%s", req.User.GetUID(), server.Name), + Name: server.Name, + Secrets: config.Config, } } @@ -2045,7 +2043,7 @@ func (m *MCPHandler) configureCompositeServer(req api.Context, compositeServer v for id, cred := range componentCreds { id, cred := id, cred // Rescope variables for closure g.Go(func() error { - modified, err := ensureCredential(ctx, req.GPTClient, cred) + modified, err := ensureCredential(ctx, req.GatewayClient, cred) if err != nil { return fmt.Errorf("failed to ensure credential for component %s: %w", id, err) } @@ -2203,7 +2201,7 @@ func (m *MCPHandler) DeconfigureServer(req api.Context) error { credCtx = fmt.Sprintf("%s-%s", req.User.GetUID(), mcpServer.Name) } - if err := m.removeMCPServerAndCred(req.Context(), req.GPTClient, mcpServer, []string{credCtx}); err != nil { + if err := m.removeMCPServerAndCred(req.Context(), req.GatewayClient, mcpServer, []string{credCtx}); err != nil { return err } @@ -2227,7 +2225,7 @@ func (m *MCPHandler) deconfigureCompositeServer(req api.Context, compositeServer for _, component := range componentServers.Items { addExtractedEnvVars(&component) - if err := m.removeMCPServerAndCred(req.Context(), req.GPTClient, component, []string{fmt.Sprintf("%s-%s", req.User.GetUID(), component.Name)}); err != nil { + if err := m.removeMCPServerAndCred(req.Context(), req.GatewayClient, component, []string{fmt.Sprintf("%s-%s", req.User.GetUID(), component.Name)}); err != nil { return err } } @@ -2241,7 +2239,7 @@ func (m *MCPHandler) deconfigureCompositeServer(req api.Context, compositeServer return fmt.Errorf("failed to list component instances: %w", err) } for _, instance := range componentInstances.Items { - if err := DeleteCredentialIfExists(req.Context(), req.GPTClient, []string{MCPServerInstanceCredentialContext(instance)}, instance.Name); err != nil { + if err := DeleteCredentialIfExists(req.Context(), req.GatewayClient, []string{MCPServerInstanceCredentialContext(instance)}, instance.Name); err != nil { return fmt.Errorf("failed to delete component instance configuration %s: %w", instance.Name, err) } _, serverConfig, err := serverFromMCPServerInstance(req, instance) @@ -2259,7 +2257,7 @@ func (m *MCPHandler) deconfigureCompositeServer(req api.Context, compositeServer scope = req.User.GetUID() credCtx = fmt.Sprintf("%s-%s", scope, compositeServer.Name) ) - if err := m.removeMCPServerAndCred(req.Context(), req.GPTClient, compositeServer, []string{credCtx}); err != nil { + if err := m.removeMCPServerAndCred(req.Context(), req.GatewayClient, compositeServer, []string{credCtx}); err != nil { return err } @@ -2302,11 +2300,11 @@ func (m *MCPHandler) Reveal(req api.Context) error { } // Non-composite: return flat env - cred, err := req.GPTClient.RevealCredential(req.Context(), []string{credCtx}, mcpServer.Name) - if err != nil && !errors.As(err, &gptscript.ErrNotFound{}) { + cred, err := req.GatewayClient.RevealCredential(req.Context(), []string{credCtx}, mcpServer.Name) + if err != nil && !errors.As(err, &gateway.CredentialNotFoundError{}) { return fmt.Errorf("failed to find credential: %w", err) } else if err == nil { - return req.Write(cred.Env) + return req.Write(cred.Secrets) } return types.NewErrNotFound("no credential found for %q", mcpServer.Name) @@ -2353,17 +2351,17 @@ func (m *MCPHandler) revealCompositeServer(req api.Context, compositeServer v1.M // For each component, reveal its credential context and URL for _, component := range componentServers.Items { - cred, err := req.GPTClient.RevealCredential( + cred, err := req.GatewayClient.RevealCredential( req.Context(), []string{fmt.Sprintf("%s-%s", req.User.GetUID(), component.Name)}, component.Name, ) - if err != nil && !errors.As(err, &gptscript.ErrNotFound{}) { + if err != nil && !errors.As(err, &gateway.CredentialNotFoundError{}) { return fmt.Errorf("failed to find credential for component %s: %w", component.Name, err) } cfg := map[string]string{} - for k, v := range cred.Env { + for k, v := range cred.Secrets { if v != "" { cfg[k] = v } @@ -2383,18 +2381,18 @@ func (m *MCPHandler) revealCompositeServer(req api.Context, compositeServer v1.M } for _, instance := range componentInstances.Items { - cred, err := req.GPTClient.RevealCredential( + cred, err := req.GatewayClient.RevealCredential( req.Context(), []string{MCPServerInstanceCredentialContext(instance)}, instance.Name, ) - if err != nil && !errors.As(err, &gptscript.ErrNotFound{}) { + if err != nil && !errors.As(err, &gateway.CredentialNotFoundError{}) { return fmt.Errorf("failed to find credential for component instance %s: %w", instance.Name, err) } mcpServerID := instance.Spec.MCPServerName result[mcpServerID] = componentConfig{ - Config: cred.Env, + Config: cred.Secrets, Disabled: disabledComponents[mcpServerID], } } @@ -2441,9 +2439,9 @@ func (m *MCPHandler) removeMCPServer(ctx context.Context, mcpServer v1.MCPServer return nil } -func (m *MCPHandler) removeMCPServerAndCred(ctx context.Context, gptClient *gptscript.GPTScript, mcpServer v1.MCPServer, credCtx []string) error { +func (m *MCPHandler) removeMCPServerAndCred(ctx context.Context, gatewayClient *gateway.Client, mcpServer v1.MCPServer, credCtx []string) error { // Delete credential if it exists - if err := DeleteCredentialIfExists(ctx, gptClient, credCtx, mcpServer.Name); err != nil { + if err := DeleteCredentialIfExists(ctx, gatewayClient, credCtx, mcpServer.Name); err != nil { return err } @@ -2821,13 +2819,13 @@ func resolveCompositeComponents(req api.Context, composite v1.MCPServer) ([]type } for _, component := range componentServers.Items { - cred, err := req.GPTClient.RevealCredential(req.Context(), []string{fmt.Sprintf("%s-%s", component.Spec.UserID, component.Name)}, component.Name) - if err != nil && !errors.As(err, &gptscript.ErrNotFound{}) { + cred, err := req.GatewayClient.RevealCredential(req.Context(), []string{fmt.Sprintf("%s-%s", component.Spec.UserID, component.Name)}, component.Name) + if err != nil && !errors.As(err, &gateway.CredentialNotFoundError{}) { return nil, fmt.Errorf("failed to reveal credential for component %s: %w", component.Name, err) } addExtractedEnvVars(&component) - mergedEnv, err := mcp.MergeBoundCreds(req.Context(), req.LocalK8sClient, req.ObotNamespace, component.Spec.Manifest.Env, component.Spec.Manifest.RemoteConfig, cred.Env) + mergedEnv, err := mcp.MergeBoundCreds(req.Context(), req.LocalK8sClient, req.ObotNamespace, component.Spec.Manifest.Env, component.Spec.Manifest.RemoteConfig, cred.Secrets) if err != nil { return nil, fmt.Errorf("failed to resolve secret bindings for component %s: %w", component.Name, err) } @@ -2898,7 +2896,7 @@ func (m *MCPHandler) ListServersFromAllSources(req api.Context) error { } } - creds, err := req.GPTClient.ListCredentials(req.Context(), gptscript.ListCredentialsOptions{ + creds, err := req.GatewayClient.ListCredentials(req.Context(), gateway.ListCredentialsOptions{ CredentialContexts: credCtxs, }) if err != nil { @@ -2907,12 +2905,12 @@ func (m *MCPHandler) ListServersFromAllSources(req api.Context) error { credMap := make(map[string]map[string]string, len(creds)) for _, cred := range creds { - if _, ok := credMap[cred.ToolName]; !ok { - c, err := req.GPTClient.RevealCredential(req.Context(), []string{cred.Context}, cred.ToolName) - if err != nil && !errors.As(err, &gptscript.ErrNotFound{}) { + if _, ok := credMap[cred.Name]; !ok { + c, err := req.GatewayClient.RevealCredential(req.Context(), []string{cred.Context}, cred.Name) + if err != nil && !errors.As(err, &gateway.CredentialNotFoundError{}) { return fmt.Errorf("failed to find credential: %w", err) } - credMap[cred.ToolName] = c.Env + credMap[cred.Name] = c.Secrets } } @@ -3007,8 +3005,8 @@ func (m *MCPHandler) GetServerFromAllSources(req api.Context) error { credCtxs = []string{fmt.Sprintf("%s-%s", server.Spec.PowerUserWorkspaceID, server.Name)} } - cred, err := req.GPTClient.RevealCredential(req.Context(), credCtxs, server.Name) - if err != nil && !errors.As(err, &gptscript.ErrNotFound{}) { + cred, err := req.GatewayClient.RevealCredential(req.Context(), credCtxs, server.Name) + if err != nil && !errors.As(err, &gateway.CredentialNotFoundError{}) { return fmt.Errorf("failed to find credential: %w", err) } @@ -3026,7 +3024,7 @@ func (m *MCPHandler) GetServerFromAllSources(req api.Context) error { // Don't fail if catalog entry is missing, just continue without preview } - mergedEnv, err := mcp.MergeBoundCreds(req.Context(), req.LocalK8sClient, req.ObotNamespace, server.Spec.Manifest.Env, server.Spec.Manifest.RemoteConfig, cred.Env) + mergedEnv, err := mcp.MergeBoundCreds(req.Context(), req.LocalK8sClient, req.ObotNamespace, server.Spec.Manifest.Env, server.Spec.Manifest.RemoteConfig, cred.Secrets) if err != nil { return fmt.Errorf("failed to resolve secret bindings: %w", err) } @@ -3388,12 +3386,12 @@ func (m *MCPHandler) RedeployWithK8sSettings(req api.Context) error { credCtxs = append(credCtxs, fmt.Sprintf("%s-%s", server.Spec.UserID, server.Name)) } - cred, err := req.GPTClient.RevealCredential(req.Context(), credCtxs, server.Name) - if err != nil && !errors.As(err, &gptscript.ErrNotFound{}) { + cred, err := req.GatewayClient.RevealCredential(req.Context(), credCtxs, server.Name) + if err != nil && !errors.As(err, &gateway.CredentialNotFoundError{}) { return fmt.Errorf("failed to find credential: %w", err) } - mergedEnv, err := mcp.MergeBoundCreds(req.Context(), req.LocalK8sClient, req.ObotNamespace, server.Spec.Manifest.Env, server.Spec.Manifest.RemoteConfig, cred.Env) + mergedEnv, err := mcp.MergeBoundCreds(req.Context(), req.LocalK8sClient, req.ObotNamespace, server.Spec.Manifest.Env, server.Spec.Manifest.RemoteConfig, cred.Secrets) if err != nil { return fmt.Errorf("failed to resolve secret bindings: %w", err) } diff --git a/pkg/api/handlers/mcp_oauth_debugger.go b/pkg/api/handlers/mcp_oauth_debugger.go index ad709137d4..37b2f8e498 100644 --- a/pkg/api/handlers/mcp_oauth_debugger.go +++ b/pkg/api/handlers/mcp_oauth_debugger.go @@ -12,10 +12,10 @@ import ( "strings" "time" - "github.com/gptscript-ai/go-gptscript" nmcp "github.com/obot-platform/nanobot/pkg/mcp" "github.com/obot-platform/obot/apiclient/types" "github.com/obot-platform/obot/pkg/api" + gateway "github.com/obot-platform/obot/pkg/gateway/client" gwtypes "github.com/obot-platform/obot/pkg/gateway/types" v1 "github.com/obot-platform/obot/pkg/storage/apis/obot.obot.ai/v1" "github.com/obot-platform/obot/pkg/system" @@ -265,11 +265,11 @@ func registerOAuthDebuggerClient(ctx context.Context, registrationEndpoint strin func (m *MCPHandler) lookupStaticOAuthClient(req api.Context, server v1.MCPServer) (string, string, error) { if server.Spec.MCPServerCatalogEntryName != "" { credName := system.MCPOAuthCredentialName(server.Spec.MCPServerCatalogEntryName) - cred, err := req.GPTClient.RevealCredential(req.Context(), []string{credName}, "oauth") - if err == nil && cred.Env["CLIENT_ID"] != "" && cred.Env["CLIENT_SECRET"] != "" { - return cred.Env["CLIENT_ID"], cred.Env["CLIENT_SECRET"], nil + cred, err := req.GatewayClient.RevealCredential(req.Context(), []string{credName}, "oauth") + if err == nil && cred.Secrets["CLIENT_ID"] != "" && cred.Secrets["CLIENT_SECRET"] != "" { + return cred.Secrets["CLIENT_ID"], cred.Secrets["CLIENT_SECRET"], nil } - if err != nil && !errors.As(err, &gptscript.ErrNotFound{}) { + if err != nil && !errors.As(err, &gateway.CredentialNotFoundError{}) { return "", "", err } } diff --git a/pkg/api/handlers/mcpcatalogs.go b/pkg/api/handlers/mcpcatalogs.go index 269d88bb57..68e8d26d71 100644 --- a/pkg/api/handlers/mcpcatalogs.go +++ b/pkg/api/handlers/mcpcatalogs.go @@ -9,7 +9,6 @@ import ( "sort" "strings" - "github.com/gptscript-ai/go-gptscript" "github.com/gptscript-ai/gptscript/pkg/hash" "github.com/obot-platform/nah/pkg/name" "github.com/obot-platform/obot/apiclient/types" @@ -17,6 +16,7 @@ import ( "github.com/obot-platform/obot/pkg/api" mcpcataloghandler "github.com/obot-platform/obot/pkg/controller/handlers/mcpcatalog" gclient "github.com/obot-platform/obot/pkg/gateway/client" + gatewaytypes "github.com/obot-platform/obot/pkg/gateway/types" "github.com/obot-platform/obot/pkg/mcp" v1 "github.com/obot-platform/obot/pkg/storage/apis/obot.obot.ai/v1" "github.com/obot-platform/obot/pkg/system" @@ -149,12 +149,12 @@ func (h *MCPCatalogHandler) Update(req api.Context) error { } // Reveal the existing single credential that holds all source-URL tokens. - existingCred, err := req.GPTClient.RevealCredential(req.Context(), []string{catalog.Name}, mcpcataloghandler.CatalogCredentialToolName) - if err != nil && !errors.As(err, &gptscript.ErrNotFound{}) { + existingCred, err := req.GatewayClient.RevealCredential(req.Context(), []string{catalog.Name}, mcpcataloghandler.CatalogCredentialToolName) + if err != nil && !errors.As(err, &gclient.CredentialNotFoundError{}) { return fmt.Errorf("failed to reveal catalog credentials: %w", err) } - newTokens := mergeCatalogTokens(manifest.SourceURLs, manifest.SourceURLCredentials, existingCred.Env) + newTokens := mergeCatalogTokens(manifest.SourceURLs, manifest.SourceURLCredentials, existingCred.Secrets) catalog.Spec.SourceURLs = manifest.SourceURLs @@ -162,7 +162,7 @@ func (h *MCPCatalogHandler) Update(req api.Context) error { return fmt.Errorf("failed to update catalog: %w", err) } - if err := storeCatalogTokens(req, catalog.Name, newTokens, existingCred.Env); err != nil { + if err := storeCatalogTokens(req, catalog.Name, newTokens, existingCred.Secrets); err != nil { return err } @@ -496,12 +496,12 @@ func (h *MCPCatalogHandler) AdminListServersForEntryInCatalog(req api.Context) e } else { credCtx = fmt.Sprintf("%s-%s", server.Spec.UserID, server.Name) } - cred, err := req.GPTClient.RevealCredential(req.Context(), []string{credCtx}, server.Name) - if err != nil && !errors.As(err, &gptscript.ErrNotFound{}) { + cred, err := req.GatewayClient.RevealCredential(req.Context(), []string{credCtx}, server.Name) + if err != nil && !errors.As(err, &gclient.CredentialNotFoundError{}) { return fmt.Errorf("failed to find credential: %w", err) } - mergedEnv, err := mcp.MergeBoundCreds(req.Context(), req.LocalK8sClient, req.ObotNamespace, server.Spec.Manifest.Env, server.Spec.Manifest.RemoteConfig, cred.Env) + mergedEnv, err := mcp.MergeBoundCreds(req.Context(), req.LocalK8sClient, req.ObotNamespace, server.Spec.Manifest.Env, server.Spec.Manifest.RemoteConfig, cred.Secrets) if err != nil { return fmt.Errorf("failed to resolve secret bindings: %w", err) } @@ -581,12 +581,12 @@ func (h *MCPCatalogHandler) AdminListServersForAllEntriesInCatalog(req api.Conte } else { credCtx = fmt.Sprintf("%s-%s", server.Spec.UserID, server.Name) } - cred, err := req.GPTClient.RevealCredential(req.Context(), []string{credCtx}, server.Name) - if err != nil && !errors.As(err, &gptscript.ErrNotFound{}) { + cred, err := req.GatewayClient.RevealCredential(req.Context(), []string{credCtx}, server.Name) + if err != nil && !errors.As(err, &gclient.CredentialNotFoundError{}) { return fmt.Errorf("failed to find credential: %w", err) } - mergedEnv, err := mcp.MergeBoundCreds(req.Context(), req.LocalK8sClient, req.ObotNamespace, server.Spec.Manifest.Env, server.Spec.Manifest.RemoteConfig, cred.Env) + mergedEnv, err := mcp.MergeBoundCreds(req.Context(), req.LocalK8sClient, req.ObotNamespace, server.Spec.Manifest.Env, server.Spec.Manifest.RemoteConfig, cred.Secrets) if err != nil { return fmt.Errorf("failed to resolve secret bindings: %w", err) } @@ -659,12 +659,12 @@ func (h *MCPCatalogHandler) ListServersForEntry(req api.Context) error { } else { credCtx = fmt.Sprintf("%s-%s", server.Spec.UserID, server.Name) } - cred, err := req.GPTClient.RevealCredential(req.Context(), []string{credCtx}, server.Name) - if err != nil && !errors.As(err, &gptscript.ErrNotFound{}) { + cred, err := req.GatewayClient.RevealCredential(req.Context(), []string{credCtx}, server.Name) + if err != nil && !errors.As(err, &gclient.CredentialNotFoundError{}) { return fmt.Errorf("failed to find credential: %w", err) } - mergedEnv, err := mcp.MergeBoundCreds(req.Context(), req.LocalK8sClient, req.ObotNamespace, server.Spec.Manifest.Env, server.Spec.Manifest.RemoteConfig, cred.Env) + mergedEnv, err := mcp.MergeBoundCreds(req.Context(), req.LocalK8sClient, req.ObotNamespace, server.Spec.Manifest.Env, server.Spec.Manifest.RemoteConfig, cred.Secrets) if err != nil { return fmt.Errorf("failed to resolve secret bindings: %w", err) } @@ -728,12 +728,12 @@ func (h *MCPCatalogHandler) GetServerFromEntry(req api.Context) error { } else { credCtx = fmt.Sprintf("%s-%s", server.Spec.UserID, server.Name) } - cred, err := req.GPTClient.RevealCredential(req.Context(), []string{credCtx}, server.Name) - if err != nil && !errors.As(err, &gptscript.ErrNotFound{}) { + cred, err := req.GatewayClient.RevealCredential(req.Context(), []string{credCtx}, server.Name) + if err != nil && !errors.As(err, &gclient.CredentialNotFoundError{}) { return fmt.Errorf("failed to find credential: %w", err) } - mergedEnv, err := mcp.MergeBoundCreds(req.Context(), req.LocalK8sClient, req.ObotNamespace, server.Spec.Manifest.Env, server.Spec.Manifest.RemoteConfig, cred.Env) + mergedEnv, err := mcp.MergeBoundCreds(req.Context(), req.LocalK8sClient, req.ObotNamespace, server.Spec.Manifest.Env, server.Spec.Manifest.RemoteConfig, cred.Secrets) if err != nil { return fmt.Errorf("failed to resolve secret bindings: %w", err) } @@ -812,7 +812,7 @@ func (h *MCPCatalogHandler) GenerateToolPreviews(req api.Context) error { } server, serverConfig, err := tempServerAndConfig( req.Context(), - req.GPTClient, + req.GatewayClient, req.Storage, req.LocalK8sClient, req.ObotNamespace, @@ -936,7 +936,7 @@ func (h *MCPCatalogHandler) generateCompositeToolPreviews(req api.Context, entry server, serverConfig, err := tempServerAndConfig( req.Context(), - req.GPTClient, + req.GatewayClient, req.Storage, req.LocalK8sClient, req.ObotNamespace, @@ -1065,7 +1065,7 @@ func (h *MCPCatalogHandler) GenerateToolPreviewsOAuthURL(req api.Context) error if catalogName == "" { catalogName = entry.Spec.PowerUserWorkspaceID } - server, serverConfig, err := tempServerAndConfig(req.Context(), req.GPTClient, req.Storage, req.LocalK8sClient, req.ObotNamespace, entry.Namespace, catalogName, entry.Spec.Manifest, configRequest.Config, configRequest.URL, h.serverURL) + server, serverConfig, err := tempServerAndConfig(req.Context(), req.GatewayClient, req.Storage, req.LocalK8sClient, req.ObotNamespace, entry.Namespace, catalogName, entry.Spec.Manifest, configRequest.Config, configRequest.URL, h.serverURL) if err != nil { return types.NewErrBadRequest("failed to create temporary server and config: %v", err) } @@ -1152,7 +1152,7 @@ func (h *MCPCatalogHandler) GenerateComponentToolPreviews(req api.Context) error // Use the manifest snapshot embedded in the composite entry for this component. server, serverConfig, err := tempServerAndConfig( req.Context(), - req.GPTClient, + req.GatewayClient, req.Storage, req.LocalK8sClient, req.ObotNamespace, @@ -1276,7 +1276,7 @@ func (h *MCPCatalogHandler) GenerateComponentToolPreviewsOAuthURL(req api.Contex server, serverConfig, err := tempServerAndConfig( req.Context(), - req.GPTClient, + req.GatewayClient, req.Storage, req.LocalK8sClient, req.ObotNamespace, @@ -1359,7 +1359,7 @@ func (h *MCPCatalogHandler) generateCompositeOAuthURLs(req api.Context, entry v1 server, serverConfig, err := tempServerAndConfig( req.Context(), - req.GPTClient, + req.GatewayClient, req.Storage, req.LocalK8sClient, req.ObotNamespace, @@ -1391,7 +1391,7 @@ func (h *MCPCatalogHandler) generateCompositeOAuthURLs(req api.Context, entry v1 return req.Write(oauthURLs) } -func tempServerAndConfig(ctx context.Context, gptClient *gptscript.GPTScript, client client.Client, localK8sClient client.Client, obotNamespace, namespace, catalogName string, entryManifest types.MCPServerCatalogEntryManifest, config map[string]string, url, baseURL string) (v1.MCPServer, mcp.ServerConfig, error) { +func tempServerAndConfig(ctx context.Context, gatewayClient *gclient.Client, client client.Client, localK8sClient client.Client, obotNamespace, namespace, catalogName string, entryManifest types.MCPServerCatalogEntryManifest, config map[string]string, url, baseURL string) (v1.MCPServer, mcp.ServerConfig, error) { // Convert catalog entry to server manifest serverManifest, err := types.MapCatalogEntryToServer(entryManifest, url, false) if err != nil { @@ -1429,11 +1429,10 @@ func tempServerAndConfig(ctx context.Context, gptClient *gptscript.GPTScript, cl "TOKEN_EXCHANGE_CLIENT_ID": fmt.Sprintf("%s:%s", namespace, clientID), "TOKEN_EXCHANGE_CLIENT_SECRET": clientSecret, } - if err := gptClient.CreateCredential(ctx, gptscript.Credential{ - Context: tempMCPServer.Name, - ToolName: tempMCPServer.Name, - Type: gptscript.CredentialTypeTool, - Env: tokenExchangeEnv, + if err := gatewayClient.UpsertCredential(ctx, gatewaytypes.Credential{ + Context: tempMCPServer.Name, + Name: tempMCPServer.Name, + Secrets: tokenExchangeEnv, }); err != nil { return v1.MCPServer{}, mcp.ServerConfig{}, fmt.Errorf("failed to create credential: %w", err) } @@ -1507,14 +1506,14 @@ func (h *MCPCatalogHandler) ListCategoriesForCatalog(req api.Context) error { // revealCatalogTokens returns the Env map from the single credential that stores // all source-URL tokens for a catalog. Returns an empty map if no credential exists. func revealCatalogTokens(req api.Context, catalogName string) (map[string]string, error) { - cred, err := req.GPTClient.RevealCredential(req.Context(), []string{catalogName}, mcpcataloghandler.CatalogCredentialToolName) + cred, err := req.GatewayClient.RevealCredential(req.Context(), []string{catalogName}, mcpcataloghandler.CatalogCredentialToolName) if err != nil { - if errors.As(err, &gptscript.ErrNotFound{}) { + if errors.As(err, &gclient.CredentialNotFoundError{}) { return nil, nil } return nil, fmt.Errorf("failed to reveal credentials for catalog %s: %w", catalogName, err) } - return cred.Env, nil + return cred.Secrets, nil } func convertMCPCatalog(catalog v1.MCPCatalog, tokenEnv map[string]string) types.MCPCatalog { @@ -1811,12 +1810,12 @@ func (h *MCPCatalogHandler) GetOAuthCredentials(req api.Context) error { // Check if credentials exist credName := system.MCPOAuthCredentialName(entry.Name) - cred, err := req.GPTClient.RevealCredential(req.Context(), []string{credName}, "oauth") + cred, err := req.GatewayClient.RevealCredential(req.Context(), []string{credName}, "oauth") configured := err == nil var clientID string if configured { - clientID = cred.Env["CLIENT_ID"] + clientID = cred.Secrets["CLIENT_ID"] } return req.Write(types.MCPServerOAuthCredentialStatus{ @@ -1845,7 +1844,7 @@ func (h *MCPCatalogHandler) SetOAuthCredentials(req api.Context) error { // Check if credentials already exist credName := system.MCPOAuthCredentialName(entry.Name) - _, err = req.GPTClient.RevealCredential(req.Context(), []string{credName}, "oauth") + _, err = req.GatewayClient.RevealCredential(req.Context(), []string{credName}, "oauth") credentialsExist := err == nil var clientID, clientSecret string @@ -1867,16 +1866,15 @@ func (h *MCPCatalogHandler) SetOAuthCredentials(req api.Context) error { clientSecret = trimmedClientSecret // Store new credential - cred := gptscript.Credential{ - Context: credName, - ToolName: "oauth", - Type: gptscript.CredentialTypeTool, - Env: map[string]string{ + cred := gatewaytypes.Credential{ + Context: credName, + Name: "oauth", + Secrets: map[string]string{ "CLIENT_ID": clientID, "CLIENT_SECRET": clientSecret, }, } - if err := req.GPTClient.CreateCredential(req.Context(), cred); err != nil { + if err := req.GatewayClient.UpsertCredential(req.Context(), cred); err != nil { return fmt.Errorf("failed to create OAuth credential: %w", err) } @@ -1909,11 +1907,9 @@ func (h *MCPCatalogHandler) DeleteOAuthCredentials(req api.Context) error { } credName := system.MCPOAuthCredentialName(entry.Name) - if err := req.GPTClient.DeleteCredential(req.Context(), credName, "oauth"); err != nil { - var notFound gptscript.ErrNotFound - if !errors.As(err, ¬Found) { - return err - } + deleted, err := req.GatewayClient.DeleteCredential(req.Context(), credName, "oauth") + if err != nil { + return err } // Best-effort cleanup of per-user OAuth tokens associated with this catalog entry. @@ -1937,5 +1933,5 @@ func (h *MCPCatalogHandler) DeleteOAuthCredentials(req api.Context) error { return fmt.Errorf("failed to trigger reconciliation: %w", err) } - return req.Write(map[string]bool{"deleted": true}) + return req.Write(map[string]bool{"deleted": deleted}) } diff --git a/pkg/api/handlers/mcpgateway/handler.go b/pkg/api/handlers/mcpgateway/handler.go index cf8508dd6a..e14b2eef2a 100644 --- a/pkg/api/handlers/mcpgateway/handler.go +++ b/pkg/api/handlers/mcpgateway/handler.go @@ -8,11 +8,11 @@ import ( "net/url" "strings" - "github.com/gptscript-ai/go-gptscript" "github.com/obot-platform/obot/apiclient/types" "github.com/obot-platform/obot/pkg/api" "github.com/obot-platform/obot/pkg/api/handlers" "github.com/obot-platform/obot/pkg/controller/handlers/systemmcpserver" + gateway "github.com/obot-platform/obot/pkg/gateway/client" "github.com/obot-platform/obot/pkg/mcp" v1 "github.com/obot-platform/obot/pkg/storage/apis/obot.obot.ai/v1" "github.com/obot-platform/obot/pkg/system" @@ -147,7 +147,7 @@ func (h *Handler) ensureSystemServerIsDeployed(req api.Context, mcpID string) (m if needsCredentials { credCtx := systemServer.Name - creds, err := req.GPTClient.ListCredentials(req.Context(), gptscript.ListCredentialsOptions{ + creds, err := req.GatewayClient.ListCredentials(req.Context(), gateway.ListCredentialsOptions{ CredentialContexts: []string{credCtx}, }) if err != nil { @@ -157,22 +157,22 @@ func (h *Handler) ensureSystemServerIsDeployed(req api.Context, mcpID string) (m secretToolName := systemmcpserver.SecretInfoToolName(systemServer.Name) for _, cred := range creds { // Skip the secret info credential — those vars go to the shim only, not the MCP server. - if cred.ToolName == secretToolName { + if cred.Name == secretToolName { continue } - credDetail, err := req.GPTClient.RevealCredential(req.Context(), []string{credCtx}, cred.ToolName) + credDetail, err := req.GatewayClient.RevealCredential(req.Context(), []string{credCtx}, cred.Name) if err != nil { continue } - maps.Copy(credEnv, credDetail.Env) + maps.Copy(credEnv, credDetail.Secrets) } } // Retrieve the token exchange credential var secretsCred map[string]string - tokenExchangeCred, err := req.GPTClient.RevealCredential(req.Context(), []string{systemServer.Name}, systemmcpserver.SecretInfoToolName(systemServer.Name)) + tokenExchangeCred, err := req.GatewayClient.RevealCredential(req.Context(), []string{systemServer.Name}, systemmcpserver.SecretInfoToolName(systemServer.Name)) if err == nil { - secretsCred = tokenExchangeCred.Env + secretsCred = tokenExchangeCred.Secrets } credEnv, err = mcp.MergeBoundCreds(req.Context(), req.LocalK8sClient, req.ObotNamespace, systemServer.Spec.Manifest.Env, systemServer.Spec.Manifest.RemoteConfig, credEnv) diff --git a/pkg/api/handlers/mcpgateway/oauth/mcpoauthhandler.go b/pkg/api/handlers/mcpgateway/oauth/mcpoauthhandler.go index af9c5d8d7d..a8def25fed 100644 --- a/pkg/api/handlers/mcpgateway/oauth/mcpoauthhandler.go +++ b/pkg/api/handlers/mcpgateway/oauth/mcpoauthhandler.go @@ -6,7 +6,6 @@ import ( "fmt" "strings" - "github.com/gptscript-ai/go-gptscript" nmcp "github.com/obot-platform/nanobot/pkg/mcp" "github.com/obot-platform/obot/apiclient/types" "github.com/obot-platform/obot/pkg/api" @@ -23,17 +22,15 @@ type MCPOAuthHandlerFactory struct { baseURL string mcpSessionManager *mcp.SessionManager client kclient.Client - gptscript *gptscript.GPTScript stateMgr *stateManager tokenStore mcp.GlobalTokenStore } -func NewMCPOAuthHandlerFactory(baseURL string, sessionManager *mcp.SessionManager, client kclient.Client, gptClient *gptscript.GPTScript, gatewayClient *client.Client, globalTokenStore mcp.GlobalTokenStore) *MCPOAuthHandlerFactory { +func NewMCPOAuthHandlerFactory(baseURL string, sessionManager *mcp.SessionManager, client kclient.Client, gatewayClient *client.Client, globalTokenStore mcp.GlobalTokenStore) *MCPOAuthHandlerFactory { return &MCPOAuthHandlerFactory{ baseURL: baseURL, mcpSessionManager: sessionManager, client: client, - gptscript: gptClient, stateMgr: newStateManager(gatewayClient), tokenStore: globalTokenStore, } @@ -99,7 +96,7 @@ func (f *MCPOAuthHandlerFactory) CheckForMCPAuth(req api.Context, mcpServer v1.M } // Remote server, check for OAuth directly - oauthHandler := f.newMCPOAuthHandler(userID, mcpID, mcpServerConfig.URL, oauthAppAuthRequestID) + oauthHandler := f.newMCPOAuthHandler(req.GatewayClient, userID, mcpID, mcpServerConfig.URL, oauthAppAuthRequestID) errChan := make(chan error, 1) go func() { @@ -136,7 +133,7 @@ func (f *MCPOAuthHandlerFactory) CheckForMCPAuth(req api.Context, mcpServer v1.M type mcpOAuthHandler struct { client kclient.Client - gptscript *gptscript.GPTScript + gatewayClient *client.Client stateMgr *stateManager mcpID string mcpURL string @@ -145,10 +142,10 @@ type mcpOAuthHandler struct { urlChan chan string } -func (f *MCPOAuthHandlerFactory) newMCPOAuthHandler(userID, mcpID, mcpURL, oauthAuthRequestID string) *mcpOAuthHandler { +func (f *MCPOAuthHandlerFactory) newMCPOAuthHandler(gatewayClient *client.Client, userID, mcpID, mcpURL, oauthAuthRequestID string) *mcpOAuthHandler { return &mcpOAuthHandler{ client: f.client, - gptscript: f.gptscript, + gatewayClient: gatewayClient, stateMgr: f.stateMgr, userID: userID, mcpID: mcpID, @@ -191,10 +188,10 @@ func (m *mcpOAuthHandler) Lookup(ctx context.Context, _ string) (string, string, // If the server was created from a catalog entry, look up OAuth credentials by catalog entry name if server.Spec.MCPServerCatalogEntryName != "" { credName := system.MCPOAuthCredentialName(server.Spec.MCPServerCatalogEntryName) - cred, err := m.gptscript.RevealCredential(ctx, []string{credName}, "oauth") + cred, err := m.gatewayClient.RevealCredential(ctx, []string{credName}, "oauth") if err == nil { - clientID := cred.Env["CLIENT_ID"] - clientSecret := cred.Env["CLIENT_SECRET"] + clientID := cred.Secrets["CLIENT_ID"] + clientSecret := cred.Secrets["CLIENT_SECRET"] if clientID != "" && clientSecret != "" { return clientID, clientSecret, nil } diff --git a/pkg/api/handlers/mcphelpers.go b/pkg/api/handlers/mcphelpers.go index 28a24d8530..ef3c166b69 100644 --- a/pkg/api/handlers/mcphelpers.go +++ b/pkg/api/handlers/mcphelpers.go @@ -9,7 +9,8 @@ import ( "net/http" "time" - "github.com/gptscript-ai/go-gptscript" + gateway "github.com/obot-platform/obot/pkg/gateway/client" + gatewaytypes "github.com/obot-platform/obot/pkg/gateway/types" ) // StreamLogsOptions configures SSE log streaming behavior. @@ -135,19 +136,16 @@ func stripDockerLogHeader(line string) string { // DeleteCredentialIfExists removes a credential if it exists. // Does not return an error if the credential is not found. -func DeleteCredentialIfExists(ctx context.Context, gptClient *gptscript.GPTScript, credCtxs []string, toolName string) error { - cred, err := gptClient.RevealCredential(ctx, credCtxs, toolName) +func DeleteCredentialIfExists(ctx context.Context, gatewayClient *gateway.Client, credCtxs []string, toolName string) error { + cred, err := gatewayClient.RevealCredential(ctx, credCtxs, toolName) if err != nil { - if errors.As(err, &gptscript.ErrNotFound{}) { + if errors.As(err, &gateway.CredentialNotFoundError{}) { return nil // Credential doesn't exist, nothing to delete } return fmt.Errorf("failed to find credential: %w", err) } - if err = gptClient.DeleteCredential(ctx, cred.Context, toolName); err != nil { - if errors.As(err, &gptscript.ErrNotFound{}) { - return nil // Already deleted - } + if _, err = gatewayClient.DeleteCredential(ctx, cred.Context, toolName); err != nil { return fmt.Errorf("failed to remove existing credential: %w", err) } return nil @@ -155,16 +153,16 @@ func DeleteCredentialIfExists(ctx context.Context, gptClient *gptscript.GPTScrip // EnsureCredential will ensure a given credential exists and it's env field is up to date. // Returns true if a credential was created or modified to match. -func ensureCredential(ctx context.Context, gptClient *gptscript.GPTScript, cred gptscript.Credential) (bool, error) { +func ensureCredential(ctx context.Context, gatewayClient *gateway.Client, cred gatewaytypes.Credential) (bool, error) { credCtx := []string{cred.Context} - existing, err := gptClient.RevealCredential(ctx, credCtx, cred.ToolName) + existing, err := gatewayClient.RevealCredential(ctx, credCtx, cred.Name) if err != nil { - if !errors.As(err, &gptscript.ErrNotFound{}) { + if !errors.As(err, &gateway.CredentialNotFoundError{}) { return false, fmt.Errorf("failed to find credential: %w", err) } // Create new credential - if err := gptClient.CreateCredential(ctx, cred); err != nil { + if err := gatewayClient.UpsertCredential(ctx, cred); err != nil { return false, fmt.Errorf("failed to create credential: %w", err) } return true, nil @@ -172,19 +170,19 @@ func ensureCredential(ctx context.Context, gptClient *gptscript.GPTScript, cred // Existing credential, check if it needs an update var modified bool - for key, env := range cred.Env { - existingEnv, ok := existing.Env[key] + for key, env := range cred.Secrets { + existingEnv, ok := existing.Secrets[key] if !ok || existingEnv != env { modified = true break } } - if !modified && len(cred.Env) == len(existing.Env) { + if !modified && len(cred.Secrets) == len(existing.Secrets) { // No update needed return false, nil } - if err := gptClient.CreateCredential(ctx, cred); err != nil { + if err := gatewayClient.UpsertCredential(ctx, cred); err != nil { return false, fmt.Errorf("failed to create credential: %w", err) } diff --git a/pkg/api/handlers/mcpwebhookvalidation.go b/pkg/api/handlers/mcpwebhookvalidation.go index b9444dfa70..909f8dff2c 100644 --- a/pkg/api/handlers/mcpwebhookvalidation.go +++ b/pkg/api/handlers/mcpwebhookvalidation.go @@ -10,10 +10,11 @@ import ( "strings" "time" - "github.com/gptscript-ai/go-gptscript" nmcp "github.com/obot-platform/nanobot/pkg/mcp" "github.com/obot-platform/obot/apiclient/types" "github.com/obot-platform/obot/pkg/api" + gateway "github.com/obot-platform/obot/pkg/gateway/client" + gatewaytypes "github.com/obot-platform/obot/pkg/gateway/types" "github.com/obot-platform/obot/pkg/mcp" v1 "github.com/obot-platform/obot/pkg/storage/apis/obot.obot.ai/v1" "github.com/obot-platform/obot/pkg/system" @@ -38,7 +39,7 @@ func (m *MCPWebhookValidationHandler) List(req api.Context) error { items := make([]types.MCPWebhookValidation, 0, len(list.Items)) for _, item := range list.Items { - credEnv, err := getCredentialsForWebhookValidation(req.Context(), req.GPTClient, item) + credEnv, err := getCredentialsForWebhookValidation(req.Context(), req.GatewayClient, item) if err != nil { return err } @@ -54,7 +55,7 @@ func (m *MCPWebhookValidationHandler) Get(req api.Context) error { return err } - credEnv, err := getCredentialsForWebhookValidation(req.Context(), req.GPTClient, webhookValidation) + credEnv, err := getCredentialsForWebhookValidation(req.Context(), req.GatewayClient, webhookValidation) if err != nil { return err } @@ -100,11 +101,10 @@ func (m *MCPWebhookValidationHandler) Create(req api.Context) error { return fmt.Errorf("failed to create mcp webhook validation: %w", err) } - if err := req.GPTClient.CreateCredential(req.Context(), gptscript.Credential{ - Context: system.MCPWebhookValidationCredentialContext, - ToolName: webhookValidation.Name, - Type: gptscript.CredentialTypeTool, - Env: secretCred, + if err := req.GatewayClient.UpsertCredential(req.Context(), gatewaytypes.Credential{ + Context: system.MCPWebhookValidationCredentialContext, + Name: webhookValidation.Name, + Secrets: secretCred, }); err != nil { _ = req.Delete(&webhookValidation) return fmt.Errorf("failed to create credential: %w", err) @@ -144,21 +144,20 @@ func (m *MCPWebhookValidationHandler) Update(req api.Context) error { webhookValidation.Spec.Manifest = manifest if secretCred != nil { - if err := req.GPTClient.CreateCredential(req.Context(), gptscript.Credential{ - Context: system.MCPWebhookValidationCredentialContext, - ToolName: webhookValidation.Name, - Type: gptscript.CredentialTypeTool, - Env: secretCred, + if err := req.GatewayClient.UpsertCredential(req.Context(), gatewaytypes.Credential{ + Context: system.MCPWebhookValidationCredentialContext, + Name: webhookValidation.Name, + Secrets: secretCred, }); err != nil { return fmt.Errorf("failed to create credential: %w", err) } } else { - cred, err := req.GPTClient.RevealCredential(req.Context(), []string{system.MCPWebhookValidationCredentialContext}, webhookValidation.Name) - if err != nil && !errors.As(err, &gptscript.ErrNotFound{}) { + cred, err := req.GatewayClient.RevealCredential(req.Context(), []string{system.MCPWebhookValidationCredentialContext}, webhookValidation.Name) + if err != nil && !errors.As(err, &gateway.CredentialNotFoundError{}) { return fmt.Errorf("failed to reveal credential: %w", err) } - secretCred = cred.Env + secretCred = cred.Secrets } if err := req.Update(&webhookValidation); err != nil { @@ -174,7 +173,7 @@ func (m *MCPWebhookValidationHandler) Delete(req api.Context) error { return err } - if err := req.GPTClient.DeleteCredential(req.Context(), system.MCPWebhookValidationCredentialContext, webhookValidation.Name); err != nil && !errors.As(err, &gptscript.ErrNotFound{}) { + if _, err := req.GatewayClient.DeleteCredential(req.Context(), system.MCPWebhookValidationCredentialContext, webhookValidation.Name); err != nil { return fmt.Errorf("failed to delete credential: %w", err) } @@ -209,11 +208,10 @@ func (m *MCPWebhookValidationHandler) Configure(req api.Context) error { } // Store credentials using GPTScript - if err := req.GPTClient.CreateCredential(req.Context(), gptscript.Credential{ - Context: system.MCPWebhookValidationCredentialContext, - ToolName: webhookValidation.Name, - Type: gptscript.CredentialTypeTool, - Env: envVars, + if err := req.GatewayClient.UpsertCredential(req.Context(), gatewaytypes.Credential{ + Context: system.MCPWebhookValidationCredentialContext, + Name: webhookValidation.Name, + Secrets: envVars, }); err != nil { return fmt.Errorf("failed to create credential: %w", err) } @@ -237,7 +235,7 @@ func (m *MCPWebhookValidationHandler) Deconfigure(req api.Context) error { return err } - if err := DeleteCredentialIfExists(req.Context(), req.GPTClient, []string{system.MCPWebhookValidationCredentialContext}, webhookValidation.Name); err != nil { + if err := DeleteCredentialIfExists(req.Context(), req.GatewayClient, []string{system.MCPWebhookValidationCredentialContext}, webhookValidation.Name); err != nil { return err } @@ -258,11 +256,11 @@ func (m *MCPWebhookValidationHandler) Reveal(req api.Context) error { return err } - cred, err := req.GPTClient.RevealCredential(req.Context(), []string{system.MCPWebhookValidationCredentialContext}, webhookValidation.Name) - if err != nil && !errors.As(err, &gptscript.ErrNotFound{}) { + cred, err := req.GatewayClient.RevealCredential(req.Context(), []string{system.MCPWebhookValidationCredentialContext}, webhookValidation.Name) + if err != nil && !errors.As(err, &gateway.CredentialNotFoundError{}) { return fmt.Errorf("failed to find credential: %w", err) } else if err == nil { - return req.Write(cred.Env) + return req.Write(cred.Secrets) } return types.NewErrNotFound("no credential found for %q", webhookValidation.Name) @@ -291,13 +289,13 @@ func convertMCPWebhookValidation(validation v1.MCPWebhookValidation, credEnv map return result } -func getCredentialsForWebhookValidation(ctx context.Context, gptClient *gptscript.GPTScript, webhookValidation v1.MCPWebhookValidation) (map[string]string, error) { - cred, err := gptClient.RevealCredential(ctx, []string{system.MCPWebhookValidationCredentialContext}, webhookValidation.Name) - if err != nil && !errors.As(err, &gptscript.ErrNotFound{}) { +func getCredentialsForWebhookValidation(ctx context.Context, gatewayClient *gateway.Client, webhookValidation v1.MCPWebhookValidation) (map[string]string, error) { + cred, err := gatewayClient.RevealCredential(ctx, []string{system.MCPWebhookValidationCredentialContext}, webhookValidation.Name) + if err != nil && !errors.As(err, &gateway.CredentialNotFoundError{}) { return nil, err } - return cred.Env, nil + return cred.Secrets, nil } func (m *MCPWebhookValidationHandler) getSystemServerForWebhookValidation(req api.Context) (v1.SystemMCPServer, error) { @@ -327,7 +325,7 @@ func (m *MCPWebhookValidationHandler) Restart(req api.Context) error { return err } - if err := checkEnabledAndConfigured(req.Context(), req.GPTClient, systemServer); err != nil { + if err := checkEnabledAndConfigured(req.Context(), req.GatewayClient, systemServer); err != nil { return err } @@ -353,7 +351,7 @@ func (m *MCPWebhookValidationHandler) Launch(req api.Context) error { return err } - if err := checkEnabledAndConfigured(req.Context(), req.GPTClient, systemServer); err != nil { + if err := checkEnabledAndConfigured(req.Context(), req.GatewayClient, systemServer); err != nil { return err } @@ -392,7 +390,7 @@ func (m *MCPWebhookValidationHandler) Logs(req api.Context) error { return err } - if err := checkEnabledAndConfigured(req.Context(), req.GPTClient, systemServer); err != nil { + if err := checkEnabledAndConfigured(req.Context(), req.GatewayClient, systemServer); err != nil { return err } @@ -422,7 +420,7 @@ func (m *MCPWebhookValidationHandler) GetDetails(req api.Context) error { return err } - if err := checkEnabledAndConfigured(req.Context(), req.GPTClient, systemServer); err != nil { + if err := checkEnabledAndConfigured(req.Context(), req.GatewayClient, systemServer); err != nil { return err } diff --git a/pkg/api/handlers/modelprovider.go b/pkg/api/handlers/modelprovider.go index 5b9e2d24a9..600fa15bd3 100644 --- a/pkg/api/handlers/modelprovider.go +++ b/pkg/api/handlers/modelprovider.go @@ -7,12 +7,13 @@ import ( "net/http" "strings" - "github.com/gptscript-ai/go-gptscript" "github.com/obot-platform/nah/pkg/name" "github.com/obot-platform/obot/apiclient/types" "github.com/obot-platform/obot/pkg/api" "github.com/obot-platform/obot/pkg/api/handlers/providers" + gateway "github.com/obot-platform/obot/pkg/gateway/client" "github.com/obot-platform/obot/pkg/gateway/server/dispatcher" + gatewaytypes "github.com/obot-platform/obot/pkg/gateway/types" "github.com/obot-platform/obot/pkg/invoke" v1 "github.com/obot-platform/obot/pkg/storage/apis/obot.obot.ai/v1" "github.com/obot-platform/obot/pkg/system" @@ -54,11 +55,11 @@ func (mp *ModelProviderHandler) ByID(req api.Context) error { var credEnvVars map[string]string if ref.Status.Tool != nil { if len(mps.RequiredConfigurationParameters) > 0 { - cred, err := req.GPTClient.RevealCredential(req.Context(), []string{string(ref.UID), system.GenericModelProviderCredentialContext}, ref.Name) - if err != nil && !errors.As(err, &gptscript.ErrNotFound{}) { + cred, err := req.GatewayClient.RevealCredential(req.Context(), []string{string(ref.UID), system.GenericModelProviderCredentialContext}, ref.Name) + if err != nil && !errors.As(err, &gateway.CredentialNotFoundError{}) { return fmt.Errorf("failed to reveal credential for model provider %q: %w", ref.Name, err) } else if err == nil { - credEnvVars = cred.Env + credEnvVars = cred.Secrets } } } @@ -88,7 +89,7 @@ func (mp *ModelProviderHandler) List(req api.Context) error { } credCtxs = append(credCtxs, system.GenericModelProviderCredentialContext) - creds, err := req.GPTClient.ListCredentials(req.Context(), gptscript.ListCredentialsOptions{ + creds, err := req.GatewayClient.ListCredentials(req.Context(), gateway.ListCredentialsOptions{ CredentialContexts: credCtxs, }) if err != nil { @@ -97,7 +98,7 @@ func (mp *ModelProviderHandler) List(req api.Context) error { credMap := make(map[string]map[string]string, len(creds)) for _, cred := range creds { - credMap[cred.Context+cred.ToolName] = cred.Env + credMap[cred.Context+cred.Name] = cred.Secrets } resp := make([]types.ModelProvider, 0, len(refList.Items)) @@ -164,7 +165,7 @@ func (mp *ModelProviderHandler) Validate(req api.Context) error { return fmt.Errorf("failed to create thread: %w", err) } - task, err := mp.invoker.SystemTask(req.Context(), req.GPTClient, thread, "validate from "+ref.Spec.Reference, "", invoke.SystemTaskOptions{Env: envs}) + task, err := mp.invoker.SystemTask(req.Context(), thread, "validate from "+ref.Spec.Reference, "", invoke.SystemTaskOptions{Env: envs}) if err != nil { return err } @@ -209,12 +210,12 @@ func (mp *ModelProviderHandler) Configure(req api.Context) error { // If this is a "global" model provider, then the generic model provider context is added to handle more git-ops-ian deployments. credCtxs := []string{string(ref.UID), system.GenericModelProviderCredentialContext} // Allow for updating credentials. The only way to update a credential is to delete the existing one and recreate it. - cred, err := req.GPTClient.RevealCredential(req.Context(), credCtxs, ref.Name) + cred, err := req.GatewayClient.RevealCredential(req.Context(), credCtxs, ref.Name) if err != nil { - if !errors.As(err, &gptscript.ErrNotFound{}) { + if !errors.As(err, &gateway.CredentialNotFoundError{}) { return fmt.Errorf("failed to find credential: %w", err) } - } else if err = req.GPTClient.DeleteCredential(req.Context(), cred.Context, ref.Name); err != nil { + } else if _, err = req.GatewayClient.DeleteCredential(req.Context(), cred.Context, ref.Name); err != nil { return fmt.Errorf("failed to remove existing credential: %w", err) } @@ -224,11 +225,10 @@ func (mp *ModelProviderHandler) Configure(req api.Context) error { } } - if err := req.GPTClient.CreateCredential(req.Context(), gptscript.Credential{ - Context: credCtxs[0], - ToolName: ref.Name, - Type: gptscript.CredentialTypeModelProvider, - Env: envVars, + if err := req.GatewayClient.UpsertCredential(req.Context(), gatewaytypes.Credential{ + Context: credCtxs[0], + Name: ref.Name, + Secrets: envVars, }); err != nil { return fmt.Errorf("failed to create credential: %w", err) } @@ -269,12 +269,12 @@ func (mp *ModelProviderHandler) Deconfigure(req api.Context) error { credCtxs = []string{fmt.Sprintf("%s-%s", projectID, ref.Name)} } - cred, err := req.GPTClient.RevealCredential(req.Context(), credCtxs, ref.Name) + cred, err := req.GatewayClient.RevealCredential(req.Context(), credCtxs, ref.Name) if err != nil { - if !errors.As(err, &gptscript.ErrNotFound{}) { + if !errors.As(err, &gateway.CredentialNotFoundError{}) { return fmt.Errorf("failed to find credential: %w", err) } - } else if err = req.GPTClient.DeleteCredential(req.Context(), cred.Context, ref.Name); err != nil { + } else if _, err = req.GatewayClient.DeleteCredential(req.Context(), cred.Context, ref.Name); err != nil { return fmt.Errorf("failed to remove existing credential: %w", err) } @@ -309,11 +309,11 @@ func (mp *ModelProviderHandler) Reveal(req api.Context) error { // If this is a "global" model provider, then the generic model provider context is added to handle more git-ops-ian deployments. credCtxs := []string{string(ref.UID), system.GenericModelProviderCredentialContext} - cred, err := req.GPTClient.RevealCredential(req.Context(), credCtxs, ref.Name) - if err != nil && !errors.As(err, &gptscript.ErrNotFound{}) { + cred, err := req.GatewayClient.RevealCredential(req.Context(), credCtxs, ref.Name) + if err != nil && !errors.As(err, &gateway.CredentialNotFoundError{}) { return fmt.Errorf("failed to reveal credential: %w", err) } else if err == nil { - return req.Write(cred.Env) + return req.Write(cred.Secrets) } return types.NewErrNotFound("no credential found for %q", ref.Name) @@ -337,11 +337,11 @@ func (mp *ModelProviderHandler) RefreshModels(req api.Context) error { var credEnvVars map[string]string if ref.Status.Tool != nil { if len(mps.RequiredConfigurationParameters) > 0 { - cred, err := req.GPTClient.RevealCredential(req.Context(), []string{string(ref.UID), system.GenericModelProviderCredentialContext}, ref.Name) - if err != nil && !errors.As(err, &gptscript.ErrNotFound{}) { + cred, err := req.GatewayClient.RevealCredential(req.Context(), []string{string(ref.UID), system.GenericModelProviderCredentialContext}, ref.Name) + if err != nil && !errors.As(err, &gateway.CredentialNotFoundError{}) { return fmt.Errorf("failed to reveal credential for model provider %q: %w", ref.Name, err) } else if err == nil { - credEnvVars = cred.Env + credEnvVars = cred.Secrets } } } diff --git a/pkg/api/handlers/poweruserworkspace.go b/pkg/api/handlers/poweruserworkspace.go index c8b2d63adf..5b5ad62a6b 100644 --- a/pkg/api/handlers/poweruserworkspace.go +++ b/pkg/api/handlers/poweruserworkspace.go @@ -4,10 +4,10 @@ import ( "errors" "fmt" - "github.com/gptscript-ai/go-gptscript" "github.com/obot-platform/obot/apiclient/types" "github.com/obot-platform/obot/pkg/accesscontrolrule" "github.com/obot-platform/obot/pkg/api" + gateway "github.com/obot-platform/obot/pkg/gateway/client" "github.com/obot-platform/obot/pkg/mcp" v1 "github.com/obot-platform/obot/pkg/storage/apis/obot.obot.ai/v1" "k8s.io/apimachinery/pkg/fields" @@ -112,7 +112,7 @@ func (p *PowerUserWorkspaceHandler) ListAllServers(req api.Context) error { var credMap map[string]map[string]string if len(credCtxs) > 0 { - creds, err := req.GPTClient.ListCredentials(req.Context(), gptscript.ListCredentialsOptions{ + creds, err := req.GatewayClient.ListCredentials(req.Context(), gateway.ListCredentialsOptions{ CredentialContexts: credCtxs, }) if err != nil { @@ -121,12 +121,12 @@ func (p *PowerUserWorkspaceHandler) ListAllServers(req api.Context) error { credMap = make(map[string]map[string]string, len(creds)) for _, cred := range creds { - if _, ok := credMap[cred.ToolName]; !ok { - c, err := req.GPTClient.RevealCredential(req.Context(), []string{cred.Context}, cred.ToolName) - if err != nil && !errors.As(err, &gptscript.ErrNotFound{}) { + if _, ok := credMap[cred.Name]; !ok { + c, err := req.GatewayClient.RevealCredential(req.Context(), []string{cred.Context}, cred.Name) + if err != nil && !errors.As(err, &gateway.CredentialNotFoundError{}) { return fmt.Errorf("failed to find credential: %w", err) } - credMap[cred.ToolName] = c.Env + credMap[cred.Name] = c.Secrets } } } @@ -217,7 +217,7 @@ func (p *PowerUserWorkspaceHandler) ListAllServersForAllEntries(req api.Context) var credMap map[string]map[string]string if len(credCtxs) > 0 { - creds, err := req.GPTClient.ListCredentials(req.Context(), gptscript.ListCredentialsOptions{ + creds, err := req.GatewayClient.ListCredentials(req.Context(), gateway.ListCredentialsOptions{ CredentialContexts: credCtxs, }) if err != nil { @@ -226,12 +226,12 @@ func (p *PowerUserWorkspaceHandler) ListAllServersForAllEntries(req api.Context) credMap = make(map[string]map[string]string, len(creds)) for _, cred := range creds { - if _, ok := credMap[cred.ToolName]; !ok { - c, err := req.GPTClient.RevealCredential(req.Context(), []string{cred.Context}, cred.ToolName) - if err != nil && !errors.As(err, &gptscript.ErrNotFound{}) { + if _, ok := credMap[cred.Name]; !ok { + c, err := req.GatewayClient.RevealCredential(req.Context(), []string{cred.Context}, cred.Name) + if err != nil && !errors.As(err, &gateway.CredentialNotFoundError{}) { return fmt.Errorf("failed to find credential: %w", err) } - credMap[cred.ToolName] = c.Env + credMap[cred.Name] = c.Secrets } } } diff --git a/pkg/api/handlers/registry/handler.go b/pkg/api/handlers/registry/handler.go index 5a995e9e8d..28ca5b8a28 100644 --- a/pkg/api/handlers/registry/handler.go +++ b/pkg/api/handlers/registry/handler.go @@ -6,11 +6,11 @@ import ( "slices" "strings" - "github.com/gptscript-ai/go-gptscript" "github.com/obot-platform/obot/apiclient/types" "github.com/obot-platform/obot/pkg/accesscontrolrule" "github.com/obot-platform/obot/pkg/api" "github.com/obot-platform/obot/pkg/api/handlers" + gateway "github.com/obot-platform/obot/pkg/gateway/client" "github.com/obot-platform/obot/pkg/mcp" v1 "github.com/obot-platform/obot/pkg/storage/apis/obot.obot.ai/v1" "github.com/obot-platform/obot/pkg/system" @@ -537,7 +537,7 @@ func (h *Handler) getCredentialsForServers( } // List credentials - creds, err := req.GPTClient.ListCredentials(req.Context(), gptscript.ListCredentialsOptions{ + creds, err := req.GatewayClient.ListCredentials(req.Context(), gateway.ListCredentialsOptions{ CredentialContexts: credCtxs, }) if err != nil { @@ -547,13 +547,13 @@ func (h *Handler) getCredentialsForServers( // Reveal and build map credMap := make(map[string]map[string]string) for _, cred := range creds { - if _, ok := credMap[cred.ToolName]; !ok { - revealed, err := req.GPTClient.RevealCredential(req.Context(), []string{cred.Context}, cred.ToolName) + if _, ok := credMap[cred.Name]; !ok { + revealed, err := req.GatewayClient.RevealCredential(req.Context(), []string{cred.Context}, cred.Name) if err != nil { // Skip if credential not found continue } - credMap[cred.ToolName] = revealed.Env + credMap[cred.Name] = revealed.Secrets } } @@ -567,13 +567,13 @@ func (h *Handler) getCredentialsForServer( ) (map[string]string, error) { ctx := h.buildCredentialContext(server, userID, catalogID, workspaceID) - revealed, err := req.GPTClient.RevealCredential(req.Context(), []string{ctx}, server.Name) + revealed, err := req.GatewayClient.RevealCredential(req.Context(), []string{ctx}, server.Name) if err != nil { // Return empty map if not found return make(map[string]string), nil } - return revealed.Env, nil + return revealed.Secrets, nil } func (h *Handler) buildCredentialContext( diff --git a/pkg/api/handlers/serverinstances.go b/pkg/api/handlers/serverinstances.go index a2c240c3b0..df8d54e763 100644 --- a/pkg/api/handlers/serverinstances.go +++ b/pkg/api/handlers/serverinstances.go @@ -8,10 +8,11 @@ import ( "slices" "strings" - "github.com/gptscript-ai/go-gptscript" "github.com/obot-platform/obot/apiclient/types" "github.com/obot-platform/obot/pkg/accesscontrolrule" "github.com/obot-platform/obot/pkg/api" + gateway "github.com/obot-platform/obot/pkg/gateway/client" + gatewaytypes "github.com/obot-platform/obot/pkg/gateway/types" v1 "github.com/obot-platform/obot/pkg/storage/apis/obot.obot.ai/v1" "github.com/obot-platform/obot/pkg/system" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -210,11 +211,10 @@ func (h *ServerInstancesHandler) ConfigureServerInstance(req api.Context) error } } - if err := req.GPTClient.CreateCredential(req.Context(), gptscript.Credential{ - Context: MCPServerInstanceCredentialContext(mcpServerInstance), - ToolName: mcpServerInstance.Name, - Type: gptscript.CredentialTypeTool, - Env: envVars, + if err := req.GatewayClient.UpsertCredential(req.Context(), gatewaytypes.Credential{ + Context: MCPServerInstanceCredentialContext(mcpServerInstance), + Name: mcpServerInstance.Name, + Secrets: envVars, }); err != nil { return fmt.Errorf("failed to create configuration: %w", err) } @@ -233,10 +233,10 @@ func (h *ServerInstancesHandler) DeconfigureServerInstance(req api.Context) erro return err } - if err := req.GPTClient.DeleteCredential( + if _, err := req.GatewayClient.DeleteCredential( req.Context(), MCPServerInstanceCredentialContext(mcpServerInstance), mcpServerInstance.Name, - ); err != nil && !errors.As(err, &gptscript.ErrNotFound{}) { + ); err != nil { return fmt.Errorf("failed to delete configuration: %w", err) } @@ -279,15 +279,15 @@ func ConvertMCPServerInstance(instance v1.MCPServerInstance, credEnv map[string] } func mcpServerInstanceCredEnv(req api.Context, instance v1.MCPServerInstance) (map[string]string, error) { - cred, err := req.GPTClient.RevealCredential(req.Context(), []string{MCPServerInstanceCredentialContext(instance)}, instance.Name) + cred, err := req.GatewayClient.RevealCredential(req.Context(), []string{MCPServerInstanceCredentialContext(instance)}, instance.Name) if err != nil { - if errors.As(err, &gptscript.ErrNotFound{}) { + if errors.As(err, &gateway.CredentialNotFoundError{}) { return nil, nil } return nil, fmt.Errorf("failed to find credential: %w", err) } - return cred.Env, nil + return cred.Secrets, nil } func MCPServerInstanceCredentialContext(instance v1.MCPServerInstance) string { diff --git a/pkg/api/handlers/systemmcpcatalogs.go b/pkg/api/handlers/systemmcpcatalogs.go index d6d89824c2..cf69a301a4 100644 --- a/pkg/api/handlers/systemmcpcatalogs.go +++ b/pkg/api/handlers/systemmcpcatalogs.go @@ -6,11 +6,12 @@ import ( "net/url" "strings" - "github.com/gptscript-ai/go-gptscript" "github.com/obot-platform/nah/pkg/name" "github.com/obot-platform/obot/apiclient/types" "github.com/obot-platform/obot/pkg/api" mcpcataloghandler "github.com/obot-platform/obot/pkg/controller/handlers/mcpcatalog" + gateway "github.com/obot-platform/obot/pkg/gateway/client" + gatewaytypes "github.com/obot-platform/obot/pkg/gateway/types" v1 "github.com/obot-platform/obot/pkg/storage/apis/obot.obot.ai/v1" "github.com/obot-platform/obot/pkg/system" "github.com/obot-platform/obot/pkg/validation" @@ -100,18 +101,18 @@ func (h *SystemMCPCatalogHandler) Update(req api.Context) error { return fmt.Errorf("failed to get system catalog: %w", err) } - existingCred, err := req.GPTClient.RevealCredential(req.Context(), []string{catalog.Name}, mcpcataloghandler.CatalogCredentialToolName) - if err != nil && !errors.As(err, &gptscript.ErrNotFound{}) { + existingCred, err := req.GatewayClient.RevealCredential(req.Context(), []string{catalog.Name}, mcpcataloghandler.CatalogCredentialToolName) + if err != nil && !errors.As(err, &gateway.CredentialNotFoundError{}) { return fmt.Errorf("failed to reveal system catalog credentials: %w", err) } - newTokens := mergeCatalogTokens(manifest.SourceURLs, manifest.SourceURLCredentials, existingCred.Env) + newTokens := mergeCatalogTokens(manifest.SourceURLs, manifest.SourceURLCredentials, existingCred.Secrets) catalog.Spec.DisplayName = manifest.DisplayName catalog.Spec.SourceURLs = manifest.SourceURLs if err := req.Update(&catalog); err != nil { return fmt.Errorf("failed to update system catalog: %w", err) } - if err := storeCatalogTokens(req, catalog.Name, newTokens, existingCred.Env); err != nil { + if err := storeCatalogTokens(req, catalog.Name, newTokens, existingCred.Secrets); err != nil { return err } @@ -321,16 +322,15 @@ func mergeCatalogTokens(sourceURLs []string, incoming, existing map[string]strin func storeCatalogTokens(req api.Context, catalogName string, tokens, existing map[string]string) error { if len(tokens) > 0 { - if err := req.GPTClient.CreateCredential(req.Context(), gptscript.Credential{ - Context: catalogName, - ToolName: mcpcataloghandler.CatalogCredentialToolName, - Type: gptscript.CredentialTypeTool, - Env: tokens, + if err := req.GatewayClient.UpsertCredential(req.Context(), gatewaytypes.Credential{ + Context: catalogName, + Name: mcpcataloghandler.CatalogCredentialToolName, + Secrets: tokens, }); err != nil { return fmt.Errorf("failed to store catalog credentials: %w", err) } } else if len(existing) > 0 { - if err := req.GPTClient.DeleteCredential(req.Context(), catalogName, mcpcataloghandler.CatalogCredentialToolName); err != nil && !errors.As(err, &gptscript.ErrNotFound{}) { + if _, err := req.GatewayClient.DeleteCredential(req.Context(), catalogName, mcpcataloghandler.CatalogCredentialToolName); err != nil { return fmt.Errorf("failed to delete catalog credentials: %w", err) } } diff --git a/pkg/api/handlers/systemmcpserver.go b/pkg/api/handlers/systemmcpserver.go index d6e4b4bd22..62b6a4e5e6 100644 --- a/pkg/api/handlers/systemmcpserver.go +++ b/pkg/api/handlers/systemmcpserver.go @@ -10,10 +10,11 @@ import ( "strings" "time" - "github.com/gptscript-ai/go-gptscript" "github.com/obot-platform/obot/apiclient/types" "github.com/obot-platform/obot/pkg/api" "github.com/obot-platform/obot/pkg/controller/handlers/systemmcpserver" + gateway "github.com/obot-platform/obot/pkg/gateway/client" + gatewaytypes "github.com/obot-platform/obot/pkg/gateway/types" "github.com/obot-platform/obot/pkg/mcp" v1 "github.com/obot-platform/obot/pkg/storage/apis/obot.obot.ai/v1" "github.com/obot-platform/obot/pkg/system" @@ -43,7 +44,7 @@ func (h *SystemMCPServerHandler) List(req api.Context) error { servers := make([]types.SystemMCPServer, 0, len(list.Items)) for _, server := range list.Items { - credEnv, err := systemmcpserver.GetCredentialsForSystemServer(req.Context(), req.GPTClient, server) + credEnv, err := systemmcpserver.GetCredentialsForSystemServer(req.Context(), req.GatewayClient, server) if err != nil { return err } @@ -60,7 +61,7 @@ func (h *SystemMCPServerHandler) Get(req api.Context) error { return err } - credEnv, err := systemmcpserver.GetCredentialsForSystemServer(req.Context(), req.GPTClient, systemServer) + credEnv, err := systemmcpserver.GetCredentialsForSystemServer(req.Context(), req.GatewayClient, systemServer) if err != nil { return err } @@ -119,7 +120,7 @@ func (h *SystemMCPServerHandler) Update(req api.Context) error { return fmt.Errorf("failed to update system MCP server: %w", err) } - credEnv, err := systemmcpserver.GetCredentialsForSystemServer(req.Context(), req.GPTClient, systemServer) + credEnv, err := systemmcpserver.GetCredentialsForSystemServer(req.Context(), req.GatewayClient, systemServer) if err != nil { return err } @@ -156,7 +157,7 @@ func (h *SystemMCPServerHandler) Configure(req api.Context) error { credCtx := systemServer.Name // Allow for updating credentials. The only way to update a credential is to delete the existing one and recreate it. - if err := DeleteCredentialIfExists(req.Context(), req.GPTClient, []string{credCtx}, systemServer.Name); err != nil { + if err := DeleteCredentialIfExists(req.Context(), req.GatewayClient, []string{credCtx}, systemServer.Name); err != nil { return err } @@ -168,11 +169,10 @@ func (h *SystemMCPServerHandler) Configure(req api.Context) error { } // Store credentials using GPTScript - if err := req.GPTClient.CreateCredential(req.Context(), gptscript.Credential{ - Context: credCtx, - ToolName: systemServer.Name, - Type: gptscript.CredentialTypeTool, - Env: envVars, + if err := req.GatewayClient.UpsertCredential(req.Context(), gatewaytypes.Credential{ + Context: credCtx, + Name: systemServer.Name, + Secrets: envVars, }); err != nil { return fmt.Errorf("failed to create credential: %w", err) } @@ -187,7 +187,7 @@ func (h *SystemMCPServerHandler) Configure(req api.Context) error { return fmt.Errorf("failed to update system MCP server: %w", err) } - credEnv, err := systemmcpserver.GetCredentialsForSystemServer(req.Context(), req.GPTClient, systemServer) + credEnv, err := systemmcpserver.GetCredentialsForSystemServer(req.Context(), req.GatewayClient, systemServer) if err != nil { return err } @@ -205,7 +205,7 @@ func (h *SystemMCPServerHandler) Deconfigure(req api.Context) error { credCtx := systemServer.Name // Delete credentials using GPTScript - if err := DeleteCredentialIfExists(req.Context(), req.GPTClient, []string{credCtx}, systemServer.Name); err != nil { + if err := DeleteCredentialIfExists(req.Context(), req.GatewayClient, []string{credCtx}, systemServer.Name); err != nil { return err } @@ -218,7 +218,7 @@ func (h *SystemMCPServerHandler) Deconfigure(req api.Context) error { return fmt.Errorf("failed to update system MCP server: %w", err) } - credEnv, err := systemmcpserver.GetCredentialsForSystemServer(req.Context(), req.GPTClient, systemServer) + credEnv, err := systemmcpserver.GetCredentialsForSystemServer(req.Context(), req.GatewayClient, systemServer) if err != nil { return err } @@ -234,7 +234,7 @@ func (h *SystemMCPServerHandler) Restart(req api.Context) error { } // Check if server is both enabled and configured - if err := checkEnabledAndConfigured(req.Context(), req.GPTClient, systemServer); err != nil { + if err := checkEnabledAndConfigured(req.Context(), req.GatewayClient, systemServer); err != nil { return err } @@ -342,7 +342,7 @@ func (h *SystemMCPServerHandler) Logs(req api.Context) error { } // Check if server is both enabled and configured - if err := checkEnabledAndConfigured(req.Context(), req.GPTClient, systemServer); err != nil { + if err := checkEnabledAndConfigured(req.Context(), req.GatewayClient, systemServer); err != nil { return err } @@ -376,7 +376,7 @@ func (h *SystemMCPServerHandler) GetTools(req api.Context) error { } // Check if server is both enabled and configured - if err := checkEnabledAndConfigured(req.Context(), req.GPTClient, systemServer); err != nil { + if err := checkEnabledAndConfigured(req.Context(), req.GatewayClient, systemServer); err != nil { return err } @@ -425,7 +425,7 @@ func (h *SystemMCPServerHandler) GetDetails(req api.Context) error { } // Check if server is both enabled and configured - if err := checkEnabledAndConfigured(req.Context(), req.GPTClient, systemServer); err != nil { + if err := checkEnabledAndConfigured(req.Context(), req.GatewayClient, systemServer); err != nil { return err } @@ -455,18 +455,18 @@ func (h *SystemMCPServerHandler) Reveal(req api.Context) error { } // Check if server is both enabled and configured - if err := checkEnabledAndConfigured(req.Context(), req.GPTClient, systemServer); err != nil { + if err := checkEnabledAndConfigured(req.Context(), req.GatewayClient, systemServer); err != nil { return err } credCtx := systemServer.Name // Reveal the credential - cred, err := req.GPTClient.RevealCredential(req.Context(), []string{credCtx}, systemServer.Name) - if err != nil && !errors.As(err, &gptscript.ErrNotFound{}) { + cred, err := req.GatewayClient.RevealCredential(req.Context(), []string{credCtx}, systemServer.Name) + if err != nil && !errors.As(err, &gateway.CredentialNotFoundError{}) { return fmt.Errorf("failed to find credential: %w", err) } else if err == nil { - return req.Write(cred.Env) + return req.Write(cred.Secrets) } return types.NewErrNotFound("no credential found for %q", systemServer.Name) @@ -475,12 +475,12 @@ func (h *SystemMCPServerHandler) Reveal(req api.Context) error { // Helper functions // checkEnabledAndConfigured verifies that a system MCP server is both enabled and configured -func checkEnabledAndConfigured(ctx context.Context, gptClient *gptscript.GPTScript, server v1.SystemMCPServer) error { +func checkEnabledAndConfigured(ctx context.Context, gatewayClient *gateway.Client, server v1.SystemMCPServer) error { if server.Spec.Manifest.Enabled != nil && !*server.Spec.Manifest.Enabled { return types.NewErrBadRequest("system MCP server is not enabled") } - if !systemmcpserver.IsSystemServerConfigured(ctx, gptClient, server) { + if !systemmcpserver.IsSystemServerConfigured(ctx, gatewayClient, server) { return types.NewErrBadRequest("system MCP server is not configured") } @@ -522,13 +522,13 @@ func convertSystemMCPServer(server v1.SystemMCPServer, credEnv map[string]string } func systemServerToServerConfig(req api.Context, server v1.SystemMCPServer) (mcp.ServerConfig, []string, error) { - credEnv, err := systemmcpserver.GetCredentialsForSystemServer(req.Context(), req.GPTClient, server) + credEnv, err := systemmcpserver.GetCredentialsForSystemServer(req.Context(), req.GatewayClient, server) if err != nil { return mcp.ServerConfig{}, nil, err } var ( - tokenExchangeCred gptscript.Credential + tokenExchangeCred gatewaytypes.Credential tokenCredErr error ) if err = retry.OnError(kwait.Backoff{ @@ -537,15 +537,15 @@ func systemServerToServerConfig(req api.Context, server v1.SystemMCPServer) (mcp Factor: 2.0, Jitter: 0.1, }, func(err error) bool { - return errors.As(err, &gptscript.ErrNotFound{}) + return errors.As(err, &gateway.CredentialNotFoundError{}) }, func() error { - tokenExchangeCred, tokenCredErr = req.GPTClient.RevealCredential(req.Context(), []string{server.Name}, systemmcpserver.SecretInfoToolName(server.Name)) + tokenExchangeCred, tokenCredErr = req.GatewayClient.RevealCredential(req.Context(), []string{server.Name}, systemmcpserver.SecretInfoToolName(server.Name)) return tokenCredErr }); err != nil { return mcp.ServerConfig{}, nil, fmt.Errorf("failed to find token exchange credential: %w", tokenCredErr) } - secretsCred := tokenExchangeCred.Env + secretsCred := tokenExchangeCred.Secrets baseURL := strings.TrimSuffix(req.APIBaseURL, "/api") audiences := server.ValidConnectURLs(baseURL) diff --git a/pkg/api/request.go b/pkg/api/request.go index f3794525a0..698ce73449 100644 --- a/pkg/api/request.go +++ b/pkg/api/request.go @@ -9,7 +9,6 @@ import ( "slices" "strconv" - "github.com/gptscript-ai/go-gptscript" "github.com/obot-platform/obot/apiclient/types" "github.com/obot-platform/obot/pkg/auth" gclient "github.com/obot-platform/obot/pkg/gateway/client" @@ -27,7 +26,6 @@ import ( type Context struct { http.ResponseWriter *http.Request - GPTClient *gptscript.GPTScript Storage storage.Client GatewayClient *gclient.Client User user.Info diff --git a/pkg/api/router/router.go b/pkg/api/router/router.go index 22c1518148..4fa2c254b5 100644 --- a/pkg/api/router/router.go +++ b/pkg/api/router/router.go @@ -32,7 +32,7 @@ func Router(ctx context.Context, services *services.Services) (http.Handler, err return nil, err } - oauthChecker := oauth.NewMCPOAuthHandlerFactory(services.ServerURL, services.MCPLoader, services.StorageClient, services.GPTClient, services.GatewayClient, services.MCPOAuthTokenStorage) + oauthChecker := oauth.NewMCPOAuthHandlerFactory(services.ServerURL, services.MCPLoader, services.StorageClient, services.GatewayClient, services.MCPOAuthTokenStorage) models := handlers.NewModelHandler(services.ModelAccessPolicyHelper) mcpCatalogs := handlers.NewMCPCatalogHandler(services.DefaultMCPCatalogPath, services.ServerURL, services.MCPRuntimeBackend, services.MCPLoader, oauthChecker, services.GatewayClient, services.AccessControlRuleHelper) @@ -55,7 +55,7 @@ func Router(ctx context.Context, services *services.Services) (http.Handler, err mcp := handlers.NewMCPHandler(services.MCPLoader, services.AccessControlRuleHelper, oauthChecker, services.MCPImagePullSecrets, services.ServerURL) mcpGateway := mcpgateway.NewHandler(services.MCPLoader) mcpAuditLogs := mcpgateway.NewAuditLogHandler() - auditLogExports := handlers.NewAuditLogExportHandler(services.GPTClient) + auditLogExports := handlers.NewAuditLogExportHandler(services.GatewayClient) serverInstances := handlers.NewServerInstancesHandler(services.AccessControlRuleHelper, services.ServerURL) systemMCPServers := handlers.NewSystemMCPServerHandler(services.MCPLoader) userDefaultRoleSettings := handlers.NewUserDefaultRoleSettingHandler() diff --git a/pkg/api/server/server.go b/pkg/api/server/server.go index e7f5856d91..4966f62228 100644 --- a/pkg/api/server/server.go +++ b/pkg/api/server/server.go @@ -11,7 +11,6 @@ import ( "strings" "time" - "github.com/gptscript-ai/go-gptscript" "github.com/obot-platform/obot/apiclient/types" "github.com/obot-platform/obot/logger" "github.com/obot-platform/obot/pkg/api" @@ -35,7 +34,6 @@ var log = logger.Package() type Server struct { storageClient storage.Client gatewayClient *gclient.Client - gptClient *gptscript.GPTScript localK8sClient kclient.Client obotNamespace string authenticator *authn.Authenticator @@ -51,7 +49,7 @@ type Server struct { otelHandler http.Handler } -func NewServer(storageClient storage.Client, gatewayClient *gclient.Client, gptClient *gptscript.GPTScript, localK8sClient kclient.Client, obotNamespace string, authn *authn.Authenticator, authz *authz.Authorizer, proxyManager *proxy.Manager, auditLogger audit.Logger, rateLimiter *ratelimiter.RateLimiter, baseURL string, oauthScopesSupported []string, registryNoAuth bool) *Server { +func NewServer(storageClient storage.Client, gatewayClient *gclient.Client, localK8sClient kclient.Client, obotNamespace string, authn *authn.Authenticator, authz *authz.Authorizer, proxyManager *proxy.Manager, auditLogger audit.Logger, rateLimiter *ratelimiter.RateLimiter, baseURL string, oauthScopesSupported []string, registryNoAuth bool) *Server { var scope string if len(oauthScopesSupported) > 0 { scope = fmt.Sprintf(", scope=\"%s\"", strings.Join(oauthScopesSupported, " ")) @@ -59,7 +57,6 @@ func NewServer(storageClient storage.Client, gatewayClient *gclient.Client, gptC s := &Server{ storageClient: storageClient, gatewayClient: gatewayClient, - gptClient: gptClient, localK8sClient: localK8sClient, obotNamespace: obotNamespace, authenticator: authn, @@ -221,7 +218,6 @@ func (s *Server) Wrap(f api.HandlerFunc) http.HandlerFunc { err = f(api.Context{ ResponseWriter: rw, Request: req, - GPTClient: s.gptClient, Storage: s.storageClient, GatewayClient: s.gatewayClient, User: user, diff --git a/pkg/auditlogexport/credentials.go b/pkg/auditlogexport/credentials.go index 872a4ad779..3787839cb8 100644 --- a/pkg/auditlogexport/credentials.go +++ b/pkg/auditlogexport/credentials.go @@ -5,8 +5,9 @@ import ( "errors" "fmt" - "github.com/gptscript-ai/go-gptscript" "github.com/obot-platform/obot/apiclient/types" + gateway "github.com/obot-platform/obot/pkg/gateway/client" + gatewaytypes "github.com/obot-platform/obot/pkg/gateway/types" ) var ( @@ -14,50 +15,50 @@ var ( storageCredentialsName = "audit-log-export-storage-credentials" ) -type GPTScriptCredentialProvider struct { - gptClient *gptscript.GPTScript +type CredentialProvider struct { + gatewayClient *gateway.Client } -func NewGPTScriptCredentialProvider(gptClient *gptscript.GPTScript) *GPTScriptCredentialProvider { - return &GPTScriptCredentialProvider{ - gptClient: gptClient, +func NewCredentialProvider(gatewayClient *gateway.Client) *CredentialProvider { + return &CredentialProvider{ + gatewayClient: gatewayClient, } } -func (g *GPTScriptCredentialProvider) GetStorageConfig(ctx context.Context) (*types.StorageConfig, error) { - credential, err := g.gptClient.RevealCredential(ctx, []string{storageCredentialsContext}, storageCredentialsName) +func (g *CredentialProvider) GetStorageConfig(ctx context.Context) (*types.StorageConfig, error) { + credential, err := g.gatewayClient.RevealCredential(ctx, []string{storageCredentialsContext}, storageCredentialsName) if err != nil { return nil, err } - provider := credential.Env["provider"] + provider := credential.Secrets["provider"] storageConfig := &types.StorageConfig{} switch provider { case string(types.StorageProviderS3): storageConfig.S3Config = &types.S3Config{ - Region: credential.Env["region"], - AccessKeyID: credential.Env["access_key_id"], - SecretAccessKey: credential.Env["secret_access_key"], + Region: credential.Secrets["region"], + AccessKeyID: credential.Secrets["access_key_id"], + SecretAccessKey: credential.Secrets["secret_access_key"], } case string(types.StorageProviderGCS): storageConfig.GCSConfig = &types.GCSConfig{ - ServiceAccountJSON: credential.Env["service_account_json"], + ServiceAccountJSON: credential.Secrets["service_account_json"], } case string(types.StorageProviderAzureBlob): storageConfig.AzureConfig = &types.AzureConfig{ - StorageAccount: credential.Env["storage_account"], - ClientID: credential.Env["client_id"], - TenantID: credential.Env["tenant_id"], - ClientSecret: credential.Env["client_secret"], + StorageAccount: credential.Secrets["storage_account"], + ClientID: credential.Secrets["client_id"], + TenantID: credential.Secrets["tenant_id"], + ClientSecret: credential.Secrets["client_secret"], } case string(types.StorageProviderCustomS3): storageConfig.CustomS3Config = &types.CustomS3Config{ - Endpoint: credential.Env["endpoint"], - Region: credential.Env["region"], - AccessKeyID: credential.Env["access_key_id"], - SecretAccessKey: credential.Env["secret_access_key"], + Endpoint: credential.Secrets["endpoint"], + Region: credential.Secrets["region"], + AccessKeyID: credential.Secrets["access_key_id"], + SecretAccessKey: credential.Secrets["secret_access_key"], } default: return nil, fmt.Errorf("unsupported provider type: %s", provider) @@ -66,16 +67,16 @@ func (g *GPTScriptCredentialProvider) GetStorageConfig(ctx context.Context) (*ty return storageConfig, nil } -func (g *GPTScriptCredentialProvider) StoreCredentials(ctx context.Context, config types.StorageProviderConfigInput) error { +func (g *CredentialProvider) StoreCredentials(ctx context.Context, config types.StorageProviderConfigInput) error { credentialData := make(map[string]string) provider := config.Provider var existingCredentialData map[string]string - credential, err := g.gptClient.RevealCredential(ctx, []string{storageCredentialsContext}, storageCredentialsName) - if err != nil && !errors.As(err, &gptscript.ErrNotFound{}) { + credential, err := g.gatewayClient.RevealCredential(ctx, []string{storageCredentialsContext}, storageCredentialsName) + if err != nil && !errors.As(err, &gateway.CredentialNotFoundError{}) { return err } else if err == nil { - existingCredentialData = credential.Env + existingCredentialData = credential.Secrets } switch provider { @@ -164,22 +165,22 @@ func (g *GPTScriptCredentialProvider) StoreCredentials(ctx context.Context, conf credentialData["provider"] = string(types.StorageProviderCustomS3) } - return g.gptClient.CreateCredential(ctx, gptscript.Credential{ - Type: gptscript.CredentialTypeTool, - Context: storageCredentialsContext, - ToolName: storageCredentialsName, - Env: credentialData, + return g.gatewayClient.UpsertCredential(ctx, gatewaytypes.Credential{ + Context: storageCredentialsContext, + Name: storageCredentialsName, + Secrets: credentialData, }) } -func (g *GPTScriptCredentialProvider) DeleteCredentials(ctx context.Context) error { - return g.gptClient.DeleteCredential(ctx, storageCredentialsContext, storageCredentialsName) +func (g *CredentialProvider) DeleteCredentials(ctx context.Context) error { + _, err := g.gatewayClient.DeleteCredential(ctx, storageCredentialsContext, storageCredentialsName) + return err } -func (g *GPTScriptCredentialProvider) TestCredentials(ctx context.Context, config types.StorageCredentialsTestRequest) error { +func (g *CredentialProvider) TestCredentials(ctx context.Context, config types.StorageCredentialsTestRequest) error { provider := config.Provider - p, err := NewStorageProvider(provider, g) + p, err := NewStorageProvider(provider) if err != nil { return err } diff --git a/pkg/auditlogexport/storage.go b/pkg/auditlogexport/storage.go index 96b9c5e37a..3fecbf54ef 100644 --- a/pkg/auditlogexport/storage.go +++ b/pkg/auditlogexport/storage.go @@ -18,11 +18,6 @@ type StorageProvider interface { Upload(ctx context.Context, config types.StorageConfig, bucket, key string, data io.Reader) error } -// CredentialProvider defines the interface for credential management. -type CredentialProvider interface { - GetStorageConfig(ctx context.Context) (*types.StorageConfig, error) -} - // blobStoreAdapter adapts blob.BlobStore to the audit log StorageProvider interface. // It creates a new BlobStore per call since the audit log system passes config per-call. type blobStoreAdapter struct { @@ -46,7 +41,7 @@ func (a *blobStoreAdapter) Upload(ctx context.Context, config types.StorageConfi } // NewStorageProvider creates a storage provider instance based on the provider type. -func NewStorageProvider(providerType types.StorageProviderType, _ CredentialProvider) (StorageProvider, error) { +func NewStorageProvider(providerType types.StorageProviderType) (StorageProvider, error) { switch providerType { case types.StorageProviderS3, types.StorageProviderGCS, diff --git a/pkg/bootstrap/bootstrap.go b/pkg/bootstrap/bootstrap.go index 9bae0b4a57..2a01040a38 100644 --- a/pkg/bootstrap/bootstrap.go +++ b/pkg/bootstrap/bootstrap.go @@ -9,7 +9,6 @@ import ( "os" "strings" - "github.com/gptscript-ai/go-gptscript" types2 "github.com/obot-platform/obot/apiclient/types" "github.com/obot-platform/obot/pkg/api" "github.com/obot-platform/obot/pkg/gateway/client" @@ -29,7 +28,7 @@ type Bootstrap struct { gatewayClient *client.Client } -func New(ctx context.Context, serverURL string, c *client.Client, g *gptscript.GPTScript, authEnabled, forceEnableBootstrap bool) (*Bootstrap, error) { +func New(ctx context.Context, serverURL string, c *client.Client, authEnabled, forceEnableBootstrap bool) (*Bootstrap, error) { if !authEnabled { // Auth is not enabled, so skip token generation. return &Bootstrap{ @@ -41,14 +40,14 @@ func New(ctx context.Context, serverURL string, c *client.Client, g *gptscript.G } token := os.Getenv("OBOT_BOOTSTRAP_TOKEN") - tokenFromCredential, exists, err := getTokenFromCredential(ctx, g) + tokenFromCredential, exists, err := getTokenFromCredential(ctx, c) if err != nil { return nil, err } if token != "" && !exists { // Save the token from the env var to the credential. - if err := saveTokenToCredential(ctx, token, g); err != nil { + if err := saveTokenToCredential(ctx, token, c); err != nil { return nil, err } } else if token == "" { @@ -65,7 +64,7 @@ func New(ctx context.Context, serverURL string, c *client.Client, g *gptscript.G token = fmt.Sprintf("%x", bytes) - if err := saveTokenToCredential(ctx, token, g); err != nil { + if err := saveTokenToCredential(ctx, token, c); err != nil { return nil, err } } @@ -94,33 +93,32 @@ func New(ctx context.Context, serverURL string, c *client.Client, g *gptscript.G return b, nil } -func getTokenFromCredential(ctx context.Context, g *gptscript.GPTScript) (string, bool, error) { - tokenCredential, err := g.RevealCredential(ctx, []string{ObotBootstrapCookie}, ObotBootstrapCookie) +func getTokenFromCredential(ctx context.Context, c *client.Client) (string, bool, error) { + tokenCredential, err := c.RevealCredential(ctx, []string{ObotBootstrapCookie}, ObotBootstrapCookie) if err != nil { - if errors.As(err, &gptscript.ErrNotFound{}) { + if errors.As(err, &client.CredentialNotFoundError{}) { return "", false, nil } return "", false, fmt.Errorf("failed to get bootstrap token credential: %w", err) } - value, ok := tokenCredential.Env["token"] + value, ok := tokenCredential.Secrets["token"] if !ok { return "", false, nil } return value, true, nil } -func saveTokenToCredential(ctx context.Context, token string, g *gptscript.GPTScript) error { - credential := gptscript.Credential{ - ToolName: ObotBootstrapCookie, - Context: ObotBootstrapCookie, - Type: gptscript.CredentialTypeTool, - Env: map[string]string{ +func saveTokenToCredential(ctx context.Context, token string, c *client.Client) error { + credential := types.Credential{ + Name: ObotBootstrapCookie, + Context: ObotBootstrapCookie, + Secrets: map[string]string{ "token": token, }, } - if err := g.CreateCredential(ctx, credential); err != nil { + if err := c.UpsertCredential(ctx, credential); err != nil { return fmt.Errorf("failed to store bootstrap token credential: %w", err) } return nil diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go index 6b127ab7f9..15ffc1c310 100644 --- a/pkg/controller/controller.go +++ b/pkg/controller/controller.go @@ -101,7 +101,7 @@ func (c *Controller) PreStart(ctx context.Context) error { return fmt.Errorf("failed to migrate published artifact visibility: %w", err) } - if err := migrateMultiUserMCPServerManifestValuesToCredentials(ctx, c.services.StorageClient, c.services.GPTClient); err != nil { + if err := migrateMultiUserMCPServerManifestValuesToCredentials(ctx, c.services.StorageClient, c.services.GatewayClient); err != nil { return fmt.Errorf("failed to migrate MCP server manifest values to credentials: %w", err) } @@ -593,7 +593,7 @@ func (c *Controller) setupLocalK8sRoutes() { c.localK8sRouter.Type(&appsv1.Deployment{}).IncludeRemoved().HandlerFunc(deploymentHandler.UpdateMCPServerStatus) c.localK8sRouter.Type(&appsv1.Deployment{}).HandlerFunc(deploymentHandler.CleanupOldIDs) - secretHandler := secret.New(c.services.MCPServerNamespace, c.services.GPTClient) + secretHandler := secret.New(c.services.MCPServerNamespace, c.services.GatewayClient) c.localK8sRouter.Type(&corev1.Secret{}).Namespace(c.services.MCPServerNamespace).HandlerFunc(secretHandler.UpdateNanobotAgentCreds) // Reconcile delete/update events for the provider token secret immediately, // instead of waiting for the periodic service-account key rotation loop. diff --git a/pkg/controller/handlers/auditlogexport/auditlogexport.go b/pkg/controller/handlers/auditlogexport/auditlogexport.go index 0b03b39d0c..442d0b0f00 100644 --- a/pkg/controller/handlers/auditlogexport/auditlogexport.go +++ b/pkg/controller/handlers/auditlogexport/auditlogexport.go @@ -8,7 +8,6 @@ import ( "strings" "time" - "github.com/gptscript-ai/go-gptscript" "github.com/obot-platform/nah/pkg/router" "github.com/obot-platform/obot/apiclient/types" "github.com/obot-platform/obot/pkg/auditlogexport" @@ -20,13 +19,13 @@ import ( type Handler struct { gatewayClient *client.Client - credProvider *auditlogexport.GPTScriptCredentialProvider + credProvider *auditlogexport.CredentialProvider } -func NewHandler(gptClient *gptscript.GPTScript, gatewayClient *client.Client) *Handler { +func NewHandler(gatewayClient *client.Client) *Handler { return &Handler{ gatewayClient: gatewayClient, - credProvider: auditlogexport.NewGPTScriptCredentialProvider(gptClient), + credProvider: auditlogexport.NewCredentialProvider(gatewayClient), } } @@ -81,7 +80,7 @@ func (h *Handler) performExport(ctx context.Context, export *v1.AuditLogExport) } // Create storage provider - storageProvider, err := auditlogexport.NewStorageProvider(provider, h.credProvider) + storageProvider, err := auditlogexport.NewStorageProvider(provider) if err != nil { return fmt.Errorf("failed to create storage provider: %w", err) } diff --git a/pkg/controller/handlers/cleanup/credentials.go b/pkg/controller/handlers/cleanup/credentials.go index c40bc82651..8b37539ec5 100644 --- a/pkg/controller/handlers/cleanup/credentials.go +++ b/pkg/controller/handlers/cleanup/credentials.go @@ -1,11 +1,9 @@ package cleanup import ( - "errors" "fmt" "strings" - "github.com/gptscript-ai/go-gptscript" "github.com/obot-platform/nah/pkg/router" "github.com/obot-platform/obot/pkg/api/handlers" gateway "github.com/obot-platform/obot/pkg/gateway/client" @@ -17,16 +15,14 @@ import ( ) type Credentials struct { - gClient *gptscript.GPTScript gatewayClient *gateway.Client mcpSessionManager *mcp.SessionManager serverURL string internalServerURL string } -func NewCredentials(gClient *gptscript.GPTScript, mcpSessionManager *mcp.SessionManager, gatewayClient *gateway.Client, serverURL, internalServerURL string) *Credentials { +func NewCredentials(mcpSessionManager *mcp.SessionManager, gatewayClient *gateway.Client, serverURL, internalServerURL string) *Credentials { return &Credentials{ - gClient: gClient, gatewayClient: gatewayClient, mcpSessionManager: mcpSessionManager, serverURL: serverURL, @@ -35,13 +31,13 @@ func NewCredentials(gClient *gptscript.GPTScript, mcpSessionManager *mcp.Session } func (c *Credentials) Remove(req router.Request, _ router.Response) error { - creds, err := c.gClient.ListCredentials(req.Ctx, gptscript.ListCredentialsOptions{ + creds, err := c.gatewayClient.ListCredentials(req.Ctx, gateway.ListCredentialsOptions{ CredentialContexts: []string{req.Object.GetName()}, }) if err != nil { return err } - localCreds, err := c.gClient.ListCredentials(req.Ctx, gptscript.ListCredentialsOptions{ + localCreds, err := c.gatewayClient.ListCredentials(req.Ctx, gateway.ListCredentialsOptions{ CredentialContexts: []string{req.Object.GetName() + "-local"}, }) if err != nil { @@ -65,7 +61,7 @@ func (c *Credentials) Remove(req router.Request, _ router.Response) error { modelProviderCredContexts = append(modelProviderCredContexts, fmt.Sprintf("%s-%s", projectName, modelProvider.Name)) } - mpCreds, err := c.gClient.ListCredentials(req.Ctx, gptscript.ListCredentialsOptions{ + mpCreds, err := c.gatewayClient.ListCredentials(req.Ctx, gateway.ListCredentialsOptions{ CredentialContexts: modelProviderCredContexts, }) if err != nil { @@ -75,7 +71,7 @@ func (c *Credentials) Remove(req router.Request, _ router.Response) error { creds = append(creds, mpCreds...) for _, cred := range creds { - if err := c.gClient.DeleteCredential(req.Ctx, cred.Context, cred.ToolName); err != nil { + if _, err := c.gatewayClient.DeleteCredential(req.Ctx, cred.Context, cred.Name); err != nil { return err } } @@ -91,7 +87,7 @@ func (c *Credentials) RemoveMCPCredentials(req router.Request, _ router.Response } // Cleanup the audit log token. - if err := c.gClient.DeleteCredential(req.Ctx, mcpServer.Name, mcpServer.Name+"-audit-log-token"); err != nil && !errors.As(err, &gptscript.ErrNotFound{}) { + if _, err := c.gatewayClient.DeleteCredential(req.Ctx, mcpServer.Name, mcpServer.Name+"-audit-log-token"); err != nil { return err } @@ -104,7 +100,7 @@ func (c *Credentials) RemoveMCPCredentials(req router.Request, _ router.Response credCtx = fmt.Sprintf("%s-%s", mcpServer.Spec.UserID, mcpServer.Name) } - creds, err := c.gClient.ListCredentials(req.Ctx, gptscript.ListCredentialsOptions{ + creds, err := c.gatewayClient.ListCredentials(req.Ctx, gateway.ListCredentialsOptions{ CredentialContexts: []string{credCtx}, }) if err != nil { @@ -112,7 +108,7 @@ func (c *Credentials) RemoveMCPCredentials(req router.Request, _ router.Response } for _, cred := range creds { - if err = c.gClient.DeleteCredential(req.Ctx, cred.Context, cred.ToolName); err != nil && !errors.As(err, &gptscript.ErrNotFound{}) { + if _, err = c.gatewayClient.DeleteCredential(req.Ctx, cred.Context, cred.Name); err != nil { return err } } @@ -131,7 +127,7 @@ func (c *Credentials) RemoveMCPInstanceCredentials(req router.Request, _ router. return err } - creds, err := c.gClient.ListCredentials(req.Ctx, gptscript.ListCredentialsOptions{ + creds, err := c.gatewayClient.ListCredentials(req.Ctx, gateway.ListCredentialsOptions{ CredentialContexts: []string{handlers.MCPServerInstanceCredentialContext(*mcpServerInstance)}, }) if err != nil { @@ -139,7 +135,7 @@ func (c *Credentials) RemoveMCPInstanceCredentials(req router.Request, _ router. } for _, cred := range creds { - if err = c.gClient.DeleteCredential(req.Ctx, cred.Context, cred.ToolName); err != nil && !errors.As(err, &gptscript.ErrNotFound{}) { + if _, err = c.gatewayClient.DeleteCredential(req.Ctx, cred.Context, cred.Name); err != nil { return err } } diff --git a/pkg/controller/handlers/imagepullsecret/imagepullsecret.go b/pkg/controller/handlers/imagepullsecret/imagepullsecret.go index ba651883ed..3506876b66 100644 --- a/pkg/controller/handlers/imagepullsecret/imagepullsecret.go +++ b/pkg/controller/handlers/imagepullsecret/imagepullsecret.go @@ -18,9 +18,9 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ecr" "github.com/aws/aws-sdk-go-v2/service/sts" ststypes "github.com/aws/aws-sdk-go-v2/service/sts/types" - "github.com/gptscript-ai/go-gptscript" "github.com/obot-platform/nah/pkg/router" apitypes "github.com/obot-platform/obot/apiclient/types" + gateway "github.com/obot-platform/obot/pkg/gateway/client" "github.com/obot-platform/obot/pkg/imagepullsecrets" "github.com/obot-platform/obot/pkg/mcp" v1 "github.com/obot-platform/obot/pkg/storage/apis/obot.obot.ai/v1" @@ -38,7 +38,7 @@ const ( ) type Handler struct { - gptClient *gptscript.GPTScript + gatewayClient *gateway.Client runtimeClient kclient.Client mcpRuntimeBackend string mcpNamespace string @@ -49,9 +49,9 @@ type Handler struct { now func() time.Time } -func New(gptClient *gptscript.GPTScript, runtimeClient kclient.Client, mcpRuntimeBackend, mcpNamespace, serviceNamespace, serviceAccountName string, staticSecrets []string, issuerURL string) *Handler { +func New(gatewayClient *gateway.Client, runtimeClient kclient.Client, mcpRuntimeBackend, mcpNamespace, serviceNamespace, serviceAccountName string, staticSecrets []string, issuerURL string) *Handler { return &Handler{ - gptClient: gptClient, + gatewayClient: gatewayClient, runtimeClient: runtimeClient, mcpRuntimeBackend: mcpRuntimeBackend, mcpNamespace: mcpNamespace, @@ -80,7 +80,7 @@ func (h *Handler) Cleanup(req router.Request, _ router.Response) error { if err := h.deleteK8sSecret(req.Ctx, secret); err != nil { return err } - if err := h.gptClient.DeleteCredential(req.Ctx, imagepullsecrets.CredentialContext, secret.Name); err != nil && !errors.As(err, &gptscript.ErrNotFound{}) { + if _, err := h.gatewayClient.DeleteCredential(req.Ctx, imagepullsecrets.CredentialContext, secret.Name); err != nil { return err } @@ -155,14 +155,14 @@ func (h *Handler) reconcileECR(req router.Request, resp router.Response, secret } func (h *Handler) revealPassword(ctx context.Context, name string) (string, error) { - credential, err := h.gptClient.RevealCredential(ctx, []string{imagepullsecrets.CredentialContext}, name) - if errors.As(err, &gptscript.ErrNotFound{}) { + credential, err := h.gatewayClient.RevealCredential(ctx, []string{imagepullsecrets.CredentialContext}, name) + if errors.As(err, &gateway.CredentialNotFoundError{}) { return "", errors.New("password is not configured") } if err != nil { return "", fmt.Errorf("failed to reveal password credential: %w", err) } - password := credential.Env[imagepullsecrets.PasswordEnvVar] + password := credential.Secrets[imagepullsecrets.PasswordEnvVar] if password == "" { return "", errors.New("password is not configured") } diff --git a/pkg/controller/handlers/mcpcatalog/mcpcatalog.go b/pkg/controller/handlers/mcpcatalog/mcpcatalog.go index 455117c44d..da3e3933b2 100644 --- a/pkg/controller/handlers/mcpcatalog/mcpcatalog.go +++ b/pkg/controller/handlers/mcpcatalog/mcpcatalog.go @@ -14,7 +14,6 @@ import ( "strings" "time" - gptscript "github.com/gptscript-ai/go-gptscript" "github.com/obot-platform/nah/pkg/apply" "github.com/obot-platform/nah/pkg/name" "github.com/obot-platform/nah/pkg/router" @@ -65,7 +64,6 @@ const ( type Handler struct { defaultCatalogPath string defaultSystemCatalogPath string - gptClient *gptscript.GPTScript gatewayClient *gclient.Client accessControlRuleHelper *accesscontrolrule.Helper mcpBackend string @@ -75,24 +73,23 @@ type Handler struct { // Returns an empty string if no credential is configured (not-found). Any other // error is logged so credential-store failures are visible in the sync status. func (h *Handler) revealCatalogCredential(ctx context.Context, catalogName, sourceURL string) string { - cred, err := h.gptClient.RevealCredential(ctx, + cred, err := h.gatewayClient.RevealCredential(ctx, []string{catalogName}, CatalogCredentialToolName, ) if err != nil { - if !errors.As(err, &gptscript.ErrNotFound{}) { + if !errors.As(err, &gclient.CredentialNotFoundError{}) { log.Errorf("failed to retrieve credential for catalog %s source %s: %v", catalogName, sourceURL, err) } return "" } - return cred.Env[sourceURL] + return cred.Secrets[sourceURL] } -func New(defaultCatalogPath, defaultSystemCatalogPath string, gptClient *gptscript.GPTScript, gatewayClient *gclient.Client, accessControlRuleHelper *accesscontrolrule.Helper, mcpBackend string) *Handler { +func New(defaultCatalogPath, defaultSystemCatalogPath string, gatewayClient *gclient.Client, accessControlRuleHelper *accesscontrolrule.Helper, mcpBackend string) *Handler { return &Handler{ defaultCatalogPath: defaultCatalogPath, defaultSystemCatalogPath: defaultSystemCatalogPath, - gptClient: gptClient, gatewayClient: gatewayClient, accessControlRuleHelper: accessControlRuleHelper, mcpBackend: mcpBackend, diff --git a/pkg/controller/handlers/mcpserver/mcpserver.go b/pkg/controller/handlers/mcpserver/mcpserver.go index f04eaf110a..1945a2077b 100644 --- a/pkg/controller/handlers/mcpserver/mcpserver.go +++ b/pkg/controller/handlers/mcpserver/mcpserver.go @@ -11,13 +11,14 @@ import ( "strings" "time" - "github.com/gptscript-ai/go-gptscript" "github.com/gptscript-ai/gptscript/pkg/hash" "github.com/obot-platform/nah/pkg/router" "github.com/obot-platform/nah/pkg/untriggered" nmcp "github.com/obot-platform/nanobot/pkg/mcp" "github.com/obot-platform/obot/apiclient/types" "github.com/obot-platform/obot/logger" + gateway "github.com/obot-platform/obot/pkg/gateway/client" + gatewaytypes "github.com/obot-platform/obot/pkg/gateway/types" "github.com/obot-platform/obot/pkg/mcp" v1 "github.com/obot-platform/obot/pkg/storage/apis/obot.obot.ai/v1" "github.com/obot-platform/obot/pkg/system" @@ -34,7 +35,7 @@ var log = logger.Package() const oauthMetadataSyncInterval = time.Hour type Handler struct { - gptClient *gptscript.GPTScript + gatewayClient *gateway.Client mcpSessionManager *mcp.SessionManager networkPolicyProviderEnabled bool defaultDenyAllEgress bool @@ -53,9 +54,9 @@ func effectiveDenyAllEgress(v *bool, domains []string, defaultWhenEmpty bool) bo return defaultWhenEmpty && len(domains) == 0 } -func New(gptClient *gptscript.GPTScript, mcpSessionManager *mcp.SessionManager, networkPolicyProviderEnabled, defaultDenyAllEgress bool, singleUserIdleShutdownDelay, multiUserIdleShutdownDelay, agentIdleShutdownDelay time.Duration, baseURL string, mcpRuntimeBackend string, mcpImagePullSecrets []string) *Handler { +func New(gatewayClient *gateway.Client, mcpSessionManager *mcp.SessionManager, networkPolicyProviderEnabled, defaultDenyAllEgress bool, singleUserIdleShutdownDelay, multiUserIdleShutdownDelay, agentIdleShutdownDelay time.Duration, baseURL string, mcpRuntimeBackend string, mcpImagePullSecrets []string) *Handler { return &Handler{ - gptClient: gptClient, + gatewayClient: gatewayClient, mcpSessionManager: mcpSessionManager, networkPolicyProviderEnabled: networkPolicyProviderEnabled, defaultDenyAllEgress: defaultDenyAllEgress, @@ -572,12 +573,12 @@ func (h *Handler) EnsureMCPServerSecretInfo(req router.Request, _ router.Respons } if server.Status.AuditLogTokenHash != "" { - cred, err := h.gptClient.RevealCredential(req.Ctx, []string{server.Name}, server.Name) + cred, err := h.gatewayClient.RevealCredential(req.Ctx, []string{server.Name}, server.Name) if err != nil { return fmt.Errorf("failed to get credential: %w", err) } - if server.Status.AuditLogTokenHash != hash.Digest(cred.Env["AUDIT_LOG_TOKEN"]) { + if server.Status.AuditLogTokenHash != hash.Digest(cred.Secrets["AUDIT_LOG_TOKEN"]) { log.Infof("Audit log token drift detected for MCP server, rotating credential: server=%s", server.Name) // Reset the audit log token hash to reset the credential. server.Status.AuditLogTokenHash = "" @@ -598,11 +599,10 @@ func (h *Handler) EnsureMCPServerSecretInfo(req router.Request, _ router.Respons auditLogToken := strings.ToLower(rand.Text() + rand.Text()) - if err := h.gptClient.CreateCredential(req.Ctx, gptscript.Credential{ - Context: server.Name, - ToolName: server.Name, - Type: gptscript.CredentialTypeTool, - Env: map[string]string{ + if err := h.gatewayClient.UpsertCredential(req.Ctx, gatewaytypes.Credential{ + Context: server.Name, + Name: server.Name, + Secrets: map[string]string{ "TOKEN_EXCHANGE_CLIENT_ID": fmt.Sprintf("%s:%s", req.Namespace, clientID), "TOKEN_EXCHANGE_CLIENT_SECRET": clientSecret, "AUDIT_LOG_TOKEN": auditLogToken, @@ -920,12 +920,12 @@ func (h *Handler) SyncOAuthMetadata(req router.Request, _ router.Response) error } else { credCtxs = []string{fmt.Sprintf("%s-%s", server.Spec.UserID, server.Name)} } - cred, err := h.gptClient.RevealCredential(req.Ctx, credCtxs, server.Name) - if err != nil && !errors.As(err, &gptscript.ErrNotFound{}) { + cred, err := h.gatewayClient.RevealCredential(req.Ctx, credCtxs, server.Name) + if err != nil && !errors.As(err, &gateway.CredentialNotFoundError{}) { return fmt.Errorf("failed to reveal credential: %w", err) } - serverConfig, missingConfig, err := mcp.ServerToServerConfig(*server, server.ValidConnectURLs(h.baseURL), h.baseURL, server.Spec.UserID, server.Name, server.Status.MCPCatalogID, cred.Env, nil) + serverConfig, missingConfig, err := mcp.ServerToServerConfig(*server, server.ValidConnectURLs(h.baseURL), h.baseURL, server.Spec.UserID, server.Name, server.Status.MCPCatalogID, cred.Secrets, nil) if err != nil { return fmt.Errorf("failed to convert MCP server to server config: %w", err) } else if len(missingConfig) > 0 { diff --git a/pkg/controller/handlers/mcpservercatalogentry/mcpservercatalogentry.go b/pkg/controller/handlers/mcpservercatalogentry/mcpservercatalogentry.go index f7f8efb3e0..7be84c3640 100644 --- a/pkg/controller/handlers/mcpservercatalogentry/mcpservercatalogentry.go +++ b/pkg/controller/handlers/mcpservercatalogentry/mcpservercatalogentry.go @@ -5,12 +5,12 @@ import ( "fmt" "slices" - "github.com/gptscript-ai/go-gptscript" "github.com/gptscript-ai/gptscript/pkg/hash" "github.com/obot-platform/nah/pkg/router" "github.com/obot-platform/obot/apiclient/types" "github.com/obot-platform/obot/logger" "github.com/obot-platform/obot/pkg/controller/handlers/mcpserver" + gclient "github.com/obot-platform/obot/pkg/gateway/client" v1 "github.com/obot-platform/obot/pkg/storage/apis/obot.obot.ai/v1" "github.com/obot-platform/obot/pkg/system" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -23,13 +23,13 @@ var log = logger.Package() // Handler handles operations for MCP server catalog entries type Handler struct { - gClient *gptscript.GPTScript + gatewayClient *gclient.Client } -// NewHandler creates a new Handler with the given GPTScript client -func NewHandler(gClient *gptscript.GPTScript) *Handler { +// NewHandler creates a new Handler with the given gateway client. +func NewHandler(gatewayClient *gclient.Client) *Handler { return &Handler{ - gClient: gClient, + gatewayClient: gatewayClient, } } @@ -233,13 +233,13 @@ func (h *Handler) CleanupUnusedOAuthCredentials(req router.Request, _ router.Res return nil } - if err := h.gClient.DeleteCredential(req.Ctx, system.MCPOAuthCredentialName(entry.Name), "oauth"); err != nil { - if errors.As(err, &gptscript.ErrNotFound{}) { - return nil - } + deleted, err := h.gatewayClient.DeleteCredential(req.Ctx, system.MCPOAuthCredentialName(entry.Name), "oauth") + if err != nil { return fmt.Errorf("failed to delete OAuth credential: %w", err) } - log.Infof("Deleted unused static OAuth credential for MCP catalog entry: entry=%s", entry.Name) + if deleted { + log.Infof("Deleted unused static OAuth credential for MCP catalog entry: entry=%s", entry.Name) + } return nil } @@ -274,12 +274,12 @@ func (h *Handler) EnsureOAuthCredentialStatus(req router.Request, _ router.Respo // Check if credentials exist credName := system.MCPOAuthCredentialName(entry.Name) - _, err := h.gClient.RevealCredential(req.Ctx, []string{credName}, "oauth") + _, err := h.gatewayClient.RevealCredential(req.Ctx, []string{credName}, "oauth") var configured bool if err == nil { configured = true - } else if !errors.As(err, &gptscript.ErrNotFound{}) { + } else if !errors.As(err, &gclient.CredentialNotFoundError{}) { return fmt.Errorf("failed to check credential status: %w", err) } @@ -304,15 +304,13 @@ func (h *Handler) RemoveOAuthCredentials(req router.Request, _ router.Response) // Build the credential name for this entry credName := system.MCPOAuthCredentialName(entry.Name) - // Delete the OAuth credential if it exists - if err := h.gClient.DeleteCredential(req.Ctx, credName, "oauth"); err != nil { - if errors.As(err, &gptscript.ErrNotFound{}) { - // Already deleted or never existed, that's fine - return nil - } + deleted, err := h.gatewayClient.DeleteCredential(req.Ctx, credName, "oauth") + if err != nil { return fmt.Errorf("failed to delete OAuth credential: %w", err) } - log.Infof("Removed static OAuth credential for deleted MCP catalog entry: entry=%s", entry.Name) + if deleted { + log.Infof("Removed static OAuth credential for deleted MCP catalog entry: entry=%s", entry.Name) + } return nil } diff --git a/pkg/controller/handlers/mcpwebhookvalidation/mcpwebhookvalidation.go b/pkg/controller/handlers/mcpwebhookvalidation/mcpwebhookvalidation.go index fd3157a071..da67a8e0e7 100644 --- a/pkg/controller/handlers/mcpwebhookvalidation/mcpwebhookvalidation.go +++ b/pkg/controller/handlers/mcpwebhookvalidation/mcpwebhookvalidation.go @@ -5,10 +5,11 @@ import ( "fmt" "maps" - "github.com/gptscript-ai/go-gptscript" "github.com/obot-platform/nah/pkg/router" "github.com/obot-platform/obot/apiclient/types" "github.com/obot-platform/obot/pkg/controller/handlers/systemmcpserver" + gateway "github.com/obot-platform/obot/pkg/gateway/client" + gatewaytypes "github.com/obot-platform/obot/pkg/gateway/types" v1 "github.com/obot-platform/obot/pkg/storage/apis/obot.obot.ai/v1" "github.com/obot-platform/obot/pkg/system" "k8s.io/apimachinery/pkg/api/equality" @@ -17,13 +18,13 @@ import ( ) type Handler struct { - gptClient *gptscript.GPTScript + gatewayClient *gateway.Client webhookBaseImage string } -func New(gptClient *gptscript.GPTScript, webhookBaseImage string) *Handler { +func New(gatewayClient *gateway.Client, webhookBaseImage string) *Handler { return &Handler{ - gptClient: gptClient, + gatewayClient: gatewayClient, webhookBaseImage: webhookBaseImage, } } @@ -81,13 +82,12 @@ func (h *Handler) EnsureSystemServer(req router.Request, _ router.Response) erro desired := desiredSystemServer(webhookValidation, h.webhookBaseImage) - webhookMCPCredential, err := h.gptClient.RevealCredential(req.Ctx, []string{desired.Name}, desired.Name) - if errors.As(err, &gptscript.ErrNotFound{}) || err == nil && !maps.Equal(cred, webhookMCPCredential.Env) { - if err := h.gptClient.CreateCredential(req.Ctx, gptscript.Credential{ - Context: desired.Name, - ToolName: desired.Name, - Type: gptscript.CredentialTypeTool, - Env: cred, + webhookMCPCredential, err := h.gatewayClient.RevealCredential(req.Ctx, []string{desired.Name}, desired.Name) + if errors.As(err, &gateway.CredentialNotFoundError{}) || err == nil && !maps.Equal(cred, webhookMCPCredential.Secrets) { + if err := h.gatewayClient.UpsertCredential(req.Ctx, gatewaytypes.Credential{ + Context: desired.Name, + Name: desired.Name, + Secrets: cred, }); err != nil { return fmt.Errorf("failed to create credential for webhook validation server %s: %w", webhookValidation.Name, err) } @@ -95,7 +95,7 @@ func (h *Handler) EnsureSystemServer(req router.Request, _ router.Response) erro return fmt.Errorf("failed to get credential for webhook validation server %s: %w", webhookValidation.Name, err) } - webhookValidation.Status.Configured = systemmcpserver.IsSystemServerConfigured(req.Ctx, h.gptClient, desired) + webhookValidation.Status.Configured = systemmcpserver.IsSystemServerConfigured(req.Ctx, h.gatewayClient, desired) var existing v1.SystemMCPServer if err = req.Get(&existing, webhookValidation.Namespace, desired.Name); apierrors.IsNotFound(err) { @@ -162,17 +162,17 @@ func desiredSystemServer(webhookValidation *v1.MCPWebhookValidation, image strin } func (h *Handler) getWebhookCredential(req router.Request, name string) (map[string]string, error) { - cred, err := h.gptClient.RevealCredential(req.Ctx, []string{system.MCPWebhookValidationCredentialContext}, name) + cred, err := h.gatewayClient.RevealCredential(req.Ctx, []string{system.MCPWebhookValidationCredentialContext}, name) if err != nil { - if errors.As(err, &gptscript.ErrNotFound{}) { + if errors.As(err, &gateway.CredentialNotFoundError{}) { return nil, nil } return nil, err } - if s := cred.Env["secret"]; s != "" { - delete(cred.Env, "secret") - cred.Env["WEBHOOK_SECRET"] = s + if s := cred.Secrets["secret"]; s != "" { + delete(cred.Secrets, "secret") + cred.Secrets["WEBHOOK_SECRET"] = s } - return cred.Env, nil + return cred.Secrets, nil } diff --git a/pkg/controller/handlers/nanobotagent/nanobotagent.go b/pkg/controller/handlers/nanobotagent/nanobotagent.go index 3723cba0b7..d0871d01a6 100644 --- a/pkg/controller/handlers/nanobotagent/nanobotagent.go +++ b/pkg/controller/handlers/nanobotagent/nanobotagent.go @@ -10,7 +10,6 @@ import ( "strings" "time" - "github.com/gptscript-ai/go-gptscript" "github.com/obot-platform/nah/pkg/backend" "github.com/obot-platform/nah/pkg/name" "github.com/obot-platform/nah/pkg/router" @@ -20,6 +19,7 @@ import ( "github.com/obot-platform/obot/pkg/alias" "github.com/obot-platform/obot/pkg/controller/handlers/toolreference" "github.com/obot-platform/obot/pkg/gateway/client" + gatewaytypes "github.com/obot-platform/obot/pkg/gateway/types" "github.com/obot-platform/obot/pkg/jwt/persistent" "github.com/obot-platform/obot/pkg/mcp" v1 "github.com/obot-platform/obot/pkg/storage/apis/obot.obot.ai/v1" @@ -39,7 +39,6 @@ const ( var log = logger.Package() type Handler struct { - gptClient *gptscript.GPTScript tokenService *persistent.TokenService gatewayClient *client.Client localK8SBackend backend.Backend @@ -48,13 +47,12 @@ type Handler struct { mcpServerNamespace string } -func New(gptClient *gptscript.GPTScript, tokenService *persistent.TokenService, gatewayClient *client.Client, localK8sRouter *router.Router, nanobotImage, serverURL, mcpServerNamespace string, mcpSessionManager *mcp.SessionManager) *Handler { +func New(tokenService *persistent.TokenService, gatewayClient *client.Client, localK8sRouter *router.Router, nanobotImage, serverURL, mcpServerNamespace string, mcpSessionManager *mcp.SessionManager) *Handler { var localK8SBackend backend.Backend if localK8sRouter != nil { localK8SBackend = localK8sRouter.Backend() } return &Handler{ - gptClient: gptClient, tokenService: tokenService, gatewayClient: gatewayClient, localK8SBackend: localK8SBackend, @@ -245,11 +243,11 @@ func (h *Handler) ensureCredentials(ctx context.Context, req router.Request, res // Check if credential exists and if the token needs refreshing var needsRefresh bool - cred, err := h.gptClient.RevealCredential(ctx, []string{credCtx}, mcpServerName) + cred, err := h.gatewayClient.RevealCredential(ctx, []string{credCtx}, mcpServerName) // Parse the env file before checking the error. - credEnvFileVars := parseEnvFile(cred.Env["NANOBOT_ENV_FILE"]) + credEnvFileVars := parseEnvFile(cred.Secrets["NANOBOT_ENV_FILE"]) if err != nil { - if _, ok := errors.AsType[gptscript.ErrNotFound](err); !ok { + if _, ok := errors.AsType[client.CredentialNotFoundError](err); !ok { return fmt.Errorf("failed to reveal credential: %w", err) } // Credential doesn't exist, needs to be created @@ -287,7 +285,7 @@ func (h *Handler) ensureCredentials(ctx context.Context, req router.Request, res if !needsRefresh && credEnvFileVars["NANOBOT_DEFAULT_MODEL"] == llmDefault && credEnvFileVars["NANOBOT_DEFAULT_MINI_MODEL"] == miniDefault && - cred.Env["NANOBOT_CONFIG_FILE"] == providerYAML { + cred.Secrets["NANOBOT_CONFIG_FILE"] == providerYAML { // Credentials are up to date return nil } @@ -321,7 +319,7 @@ func (h *Handler) ensureCredentials(ctx context.Context, req router.Request, res apiKeyIDStr := credEnvFileVars["MCP_API_KEY_ID_PREV"] if apiKeyIDStr == "" { // Backward compatibility while migrating old credentials. - apiKeyIDStr = cred.Env["MCP_API_KEY_ID"] + apiKeyIDStr = cred.Secrets["MCP_API_KEY_ID"] } if apiKeyIDStr != "" { if id, err := strconv.ParseUint(apiKeyIDStr, 10, 32); err == nil { @@ -366,11 +364,10 @@ func (h *Handler) ensureCredentials(ctx context.Context, req router.Request, res } // Create or update the credential with the new token, API key, and provider config. - if err := h.gptClient.CreateCredential(ctx, gptscript.Credential{ - Context: credCtx, - ToolName: mcpServerName, - Type: gptscript.CredentialTypeTool, - Env: map[string]string{ + if err := h.gatewayClient.UpsertCredential(ctx, gatewaytypes.Credential{ + Context: credCtx, + Name: mcpServerName, + Secrets: map[string]string{ "NANOBOT_ENV_FILE": strings.Join(envFileLines, "\n"), "NANOBOT_CONFIG_FILE": providerYAML, }, @@ -595,9 +592,9 @@ func (h *Handler) deleteTokens(ctx context.Context, agent *v1.NanobotAgent, mcpS credCtx := fmt.Sprintf("%s-%s", agent.Spec.UserID, mcpServerName) // Retrieve the credential to get the API key ID - cred, err := h.gptClient.RevealCredential(ctx, []string{credCtx}, mcpServerName) + cred, err := h.gatewayClient.RevealCredential(ctx, []string{credCtx}, mcpServerName) if err != nil { - if errors.As(err, &gptscript.ErrNotFound{}) { + if errors.As(err, &client.CredentialNotFoundError{}) { // Credential doesn't exist, nothing to delete return nil } @@ -606,7 +603,7 @@ func (h *Handler) deleteTokens(ctx context.Context, agent *v1.NanobotAgent, mcpS // Extract and delete the API key if present - if apiKeyIDStr := parseEnvFile(cred.Env["NANOBOT_ENV_FILE"])["MCP_API_KEY_ID"]; apiKeyIDStr != "" { + if apiKeyIDStr := parseEnvFile(cred.Secrets["NANOBOT_ENV_FILE"])["MCP_API_KEY_ID"]; apiKeyIDStr != "" { apiKeyID, err := strconv.ParseUint(apiKeyIDStr, 10, 32) if err != nil { return fmt.Errorf("failed to parse API key ID: %w", err) diff --git a/pkg/controller/handlers/oauthclients/oauthclients.go b/pkg/controller/handlers/oauthclients/oauthclients.go index a071760d38..8808593e97 100644 --- a/pkg/controller/handlers/oauthclients/oauthclients.go +++ b/pkg/controller/handlers/oauthclients/oauthclients.go @@ -1,20 +1,18 @@ package oauthclients import ( - "errors" - - "github.com/gptscript-ai/go-gptscript" "github.com/obot-platform/nah/pkg/router" + gateway "github.com/obot-platform/obot/pkg/gateway/client" v1 "github.com/obot-platform/obot/pkg/storage/apis/obot.obot.ai/v1" ) type Handler struct { - gptClient *gptscript.GPTScript + gatewayClient *gateway.Client } -func NewHandler(gptClient *gptscript.GPTScript) *Handler { +func NewHandler(gatewayClient *gateway.Client) *Handler { return &Handler{ - gptClient: gptClient, + gatewayClient: gatewayClient, } } @@ -25,9 +23,6 @@ func (h *Handler) CleanupOAuthClientCred(req router.Request, _ router.Response) return nil } - if err := h.gptClient.DeleteCredential(req.Ctx, o.Spec.MCPServerName, o.Spec.MCPServerName); !errors.As(err, &gptscript.ErrNotFound{}) { - return err - } - - return nil + _, err := h.gatewayClient.DeleteCredential(req.Ctx, o.Spec.MCPServerName, o.Spec.MCPServerName) + return err } diff --git a/pkg/controller/handlers/secret/nanobotagent.go b/pkg/controller/handlers/secret/nanobotagent.go index 30c552989d..7b8b1b7837 100644 --- a/pkg/controller/handlers/secret/nanobotagent.go +++ b/pkg/controller/handlers/secret/nanobotagent.go @@ -5,20 +5,20 @@ import ( "fmt" "strings" - "github.com/gptscript-ai/go-gptscript" "github.com/obot-platform/nah/pkg/router" + gateway "github.com/obot-platform/obot/pkg/gateway/client" corev1 "k8s.io/api/core/v1" ) type Handler struct { mcpDeploymentNamespace string - gptClient *gptscript.GPTScript + gatewayClient *gateway.Client } -func New(mcpNamespace string, gptClient *gptscript.GPTScript) *Handler { +func New(mcpNamespace string, gatewayClient *gateway.Client) *Handler { return &Handler{ mcpDeploymentNamespace: mcpNamespace, - gptClient: gptClient, + gatewayClient: gatewayClient, } } @@ -34,20 +34,20 @@ func (h *Handler) UpdateNanobotAgentCreds(req router.Request, _ router.Response) return nil } - cred, err := h.gptClient.RevealCredential(req.Ctx, []string{fmt.Sprintf("%s-%s", userID, mcpServerID)}, mcpServerID) + cred, err := h.gatewayClient.RevealCredential(req.Ctx, []string{fmt.Sprintf("%s-%s", userID, mcpServerID)}, mcpServerID) if err != nil { - if errors.As(err, &gptscript.ErrNotFound{}) { + if errors.As(err, &gateway.CredentialNotFoundError{}) { return nil } return fmt.Errorf("failed to reveal credential: %w", err) } - update := len(secret.Data) != len(cred.Env) - for key, val := range cred.Env { + update := len(secret.Data) != len(cred.Secrets) + for key, val := range cred.Secrets { if string(secret.Data[fmt.Sprintf("%s-%s", mcpServerID, key)]) != val { update = true if secret.Data == nil { - secret.Data = make(map[string][]byte, len(cred.Env)) + secret.Data = make(map[string][]byte, len(cred.Secrets)) } secret.Data[fmt.Sprintf("%s-%s", mcpServerID, key)] = []byte(val) } diff --git a/pkg/controller/handlers/systemmcpserver/systemmcpserver.go b/pkg/controller/handlers/systemmcpserver/systemmcpserver.go index d5ef9316b2..caac270d88 100644 --- a/pkg/controller/handlers/systemmcpserver/systemmcpserver.go +++ b/pkg/controller/handlers/systemmcpserver/systemmcpserver.go @@ -9,12 +9,13 @@ import ( "strings" "time" - "github.com/gptscript-ai/go-gptscript" "github.com/gptscript-ai/gptscript/pkg/hash" "github.com/obot-platform/nah/pkg/router" "github.com/obot-platform/nah/pkg/untriggered" "github.com/obot-platform/obot/apiclient/types" "github.com/obot-platform/obot/logger" + gateway "github.com/obot-platform/obot/pkg/gateway/client" + gatewaytypes "github.com/obot-platform/obot/pkg/gateway/types" "github.com/obot-platform/obot/pkg/mcp" v1 "github.com/obot-platform/obot/pkg/storage/apis/obot.obot.ai/v1" "github.com/obot-platform/obot/pkg/system" @@ -29,14 +30,14 @@ import ( var log = logger.Package() type Handler struct { - gptClient *gptscript.GPTScript + gatewayClient *gateway.Client mcpSessionManager *mcp.SessionManager serverURL string } -func New(gptClient *gptscript.GPTScript, mcpLoader *mcp.SessionManager, serverURL string) *Handler { +func New(gatewayClient *gateway.Client, mcpLoader *mcp.SessionManager, serverURL string) *Handler { return &Handler{ - gptClient: gptClient, + gatewayClient: gatewayClient, mcpSessionManager: mcpLoader, serverURL: serverURL, } @@ -70,12 +71,12 @@ func (h *Handler) EnsureSecretInfo(req router.Request, _ router.Response) error secretCredToolName := SecretInfoToolName(systemServer.Name) if systemServer.Status.AuditLogTokenHash != "" { - cred, err := h.gptClient.RevealCredential(req.Ctx, []string{systemServer.Name}, secretCredToolName) + cred, err := h.gatewayClient.RevealCredential(req.Ctx, []string{systemServer.Name}, secretCredToolName) if err != nil { return fmt.Errorf("failed to get credential: %w", err) } - if systemServer.Status.AuditLogTokenHash != hash.Digest(cred.Env["AUDIT_LOG_TOKEN"]) { + if systemServer.Status.AuditLogTokenHash != hash.Digest(cred.Secrets["AUDIT_LOG_TOKEN"]) { // Reset the audit log token hash to reset the credential. systemServer.Status.AuditLogTokenHash = "" } @@ -94,11 +95,10 @@ func (h *Handler) EnsureSecretInfo(req router.Request, _ router.Response) error auditLogToken := strings.ToLower(rand.Text() + rand.Text()) - if err := h.gptClient.CreateCredential(req.Ctx, gptscript.Credential{ - Context: systemServer.Name, - ToolName: secretCredToolName, - Type: gptscript.CredentialTypeTool, - Env: map[string]string{ + if err := h.gatewayClient.UpsertCredential(req.Ctx, gatewaytypes.Credential{ + Context: systemServer.Name, + Name: secretCredToolName, + Secrets: map[string]string{ "TOKEN_EXCHANGE_CLIENT_ID": fmt.Sprintf("%s:%s", req.Namespace, clientID), "TOKEN_EXCHANGE_CLIENT_SECRET": clientSecret, "AUDIT_LOG_TOKEN": auditLogToken, @@ -150,7 +150,7 @@ func (h *Handler) EnsureDeployment(req router.Request, _ router.Response) error } // Check if server is fully configured - if !IsSystemServerConfigured(req.Ctx, h.gptClient, *systemServer) { + if !IsSystemServerConfigured(req.Ctx, h.gatewayClient, *systemServer) { log.Infof("System MCP server %s is not fully configured, shutting down any existing deployment", systemServer.Name) // Server is not fully configured, ensure any existing deployment is removed err := h.mcpSessionManager.ShutdownIdleServer(req.Ctx, systemServer.Name) @@ -162,7 +162,7 @@ func (h *Handler) EnsureDeployment(req router.Request, _ router.Response) error // Get credentials for deployment credCtx := systemServer.Name - creds, err := h.gptClient.ListCredentials(req.Ctx, gptscript.ListCredentialsOptions{ + creds, err := h.gatewayClient.ListCredentials(req.Ctx, gateway.ListCredentialsOptions{ CredentialContexts: []string{credCtx}, }) if err != nil { @@ -173,21 +173,21 @@ func (h *Handler) EnsureDeployment(req router.Request, _ router.Response) error credEnv := make(map[string]string) for _, cred := range creds { // Skip the secret info credential — those vars go to the shim only, not the MCP server. - if cred.ToolName == secretToolName { + if cred.Name == secretToolName { continue } // Get credential details - credDetail, err := h.gptClient.RevealCredential(req.Ctx, []string{credCtx}, cred.ToolName) + credDetail, err := h.gatewayClient.RevealCredential(req.Ctx, []string{credCtx}, cred.Name) if err != nil { continue } - maps.Copy(credEnv, credDetail.Env) + maps.Copy(credEnv, credDetail.Secrets) } // Retrieve the token exchange credential var ( - tokenExchangeCred gptscript.Credential + tokenExchangeCred gatewaytypes.Credential tokenCredErr error ) if err = retry.OnError(kwait.Backoff{ @@ -196,15 +196,15 @@ func (h *Handler) EnsureDeployment(req router.Request, _ router.Response) error Factor: 2.0, Jitter: 0.1, }, func(err error) bool { - return errors.As(err, &gptscript.ErrNotFound{}) + return errors.As(err, &gateway.CredentialNotFoundError{}) }, func() error { - tokenExchangeCred, tokenCredErr = h.gptClient.RevealCredential(req.Ctx, []string{systemServer.Name}, secretToolName) + tokenExchangeCred, tokenCredErr = h.gatewayClient.RevealCredential(req.Ctx, []string{systemServer.Name}, secretToolName) return tokenCredErr }); err != nil { return fmt.Errorf("failed to find token exchange credential: %w", tokenCredErr) } - secretsCred := tokenExchangeCred.Env + secretsCred := tokenExchangeCred.Secrets audiences := systemServer.ValidConnectURLs(h.serverURL) @@ -239,7 +239,7 @@ func (h *Handler) EnsureDeployment(req router.Request, _ router.Response) error // CleanupDeployment handles cleanup when SystemMCPServer is deleted func (h *Handler) CleanupDeployment(req router.Request, _ router.Response) error { systemServer := req.Object.(*v1.SystemMCPServer) - creds, err := h.gptClient.ListCredentials(req.Ctx, gptscript.ListCredentialsOptions{ + creds, err := h.gatewayClient.ListCredentials(req.Ctx, gateway.ListCredentialsOptions{ CredentialContexts: []string{systemServer.Name}, }) if err != nil { @@ -247,8 +247,8 @@ func (h *Handler) CleanupDeployment(req router.Request, _ router.Response) error } for _, cred := range creds { - if err := h.gptClient.DeleteCredential(req.Ctx, cred.Context, cred.ToolName); err != nil && !errors.As(err, &gptscript.ErrNotFound{}) { - return fmt.Errorf("failed to delete credential %s: %w", cred.ToolName, err) + if _, err := h.gatewayClient.DeleteCredential(req.Ctx, cred.Context, cred.Name); err != nil { + return fmt.Errorf("failed to delete credential %s: %w", cred.Name, err) } } @@ -262,8 +262,8 @@ func (h *Handler) CleanupDeployment(req router.Request, _ router.Response) error } // IsSystemServerConfigured checks if all required configuration is present -func IsSystemServerConfigured(ctx context.Context, gptClient *gptscript.GPTScript, server v1.SystemMCPServer) bool { - credEnv, err := GetCredentialsForSystemServer(ctx, gptClient, server) +func IsSystemServerConfigured(ctx context.Context, gatewayClient *gateway.Client, server v1.SystemMCPServer) bool { + credEnv, err := GetCredentialsForSystemServer(ctx, gatewayClient, server) if err != nil { log.Errorf("Failed to get credentials for system MCP server %s: %v", server.Name, err) return false @@ -281,9 +281,9 @@ func IsSystemServerConfigured(ctx context.Context, gptClient *gptscript.GPTScrip } // GetCredentialsForSystemServer retrieves all credentials for the given system MCP server and returns them as a single map of env vars. -func GetCredentialsForSystemServer(ctx context.Context, gptClient *gptscript.GPTScript, server v1.SystemMCPServer) (map[string]string, error) { +func GetCredentialsForSystemServer(ctx context.Context, gatewayClient *gateway.Client, server v1.SystemMCPServer) (map[string]string, error) { credCtx := server.Name - creds, err := gptClient.ListCredentials(ctx, gptscript.ListCredentialsOptions{ + creds, err := gatewayClient.ListCredentials(ctx, gateway.ListCredentialsOptions{ CredentialContexts: []string{credCtx}, }) if err != nil { @@ -294,15 +294,15 @@ func GetCredentialsForSystemServer(ctx context.Context, gptClient *gptscript.GPT credEnv := make(map[string]string) for _, cred := range creds { // Skip the secret info credential — those vars go to the shim only, not the MCP server. - if cred.ToolName == secretToolName { + if cred.Name == secretToolName { continue } - credDetail, err := gptClient.RevealCredential(ctx, []string{credCtx}, cred.ToolName) + credDetail, err := gatewayClient.RevealCredential(ctx, []string{credCtx}, cred.Name) if err != nil { continue } - maps.Copy(credEnv, credDetail.Env) + maps.Copy(credEnv, credDetail.Secrets) } return credEnv, nil diff --git a/pkg/controller/handlers/toolreference/toolreference.go b/pkg/controller/handlers/toolreference/toolreference.go index b70652b07d..ff0573dd6d 100644 --- a/pkg/controller/handlers/toolreference/toolreference.go +++ b/pkg/controller/handlers/toolreference/toolreference.go @@ -21,10 +21,11 @@ import ( "github.com/obot-platform/obot/logger" "github.com/obot-platform/obot/pkg/api/handlers/providers" "github.com/obot-platform/obot/pkg/controller/creds" + gateway "github.com/obot-platform/obot/pkg/gateway/client" "github.com/obot-platform/obot/pkg/gateway/server/dispatcher" + gatewaytypes "github.com/obot-platform/obot/pkg/gateway/types" v1 "github.com/obot-platform/obot/pkg/storage/apis/obot.obot.ai/v1" "github.com/obot-platform/obot/pkg/system" - "github.com/obot-platform/obot/pkg/tools" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" "sigs.k8s.io/controller-runtime/pkg/client" @@ -77,15 +78,17 @@ func indexEntryCount(idx index) int { type Handler struct { gptClient *gptscript.GPTScript + gatewayClient *gateway.Client dispatcher *dispatcher.Dispatcher registryURLs []string lastChecksLock *sync.RWMutex lastChecks map[string]time.Time } -func New(gptClient *gptscript.GPTScript, dispatcher *dispatcher.Dispatcher, registryURLs []string) *Handler { +func New(gptClient *gptscript.GPTScript, gatewayClient *gateway.Client, dispatcher *dispatcher.Dispatcher, registryURLs []string) *Handler { return &Handler{ gptClient: gptClient, + gatewayClient: gatewayClient, dispatcher: dispatcher, registryURLs: registryURLs, lastChecks: make(map[string]time.Time), @@ -99,7 +102,7 @@ func (h *Handler) toolsToToolReferences(ctx context.Context, toolType v1.ToolRef entry.Reference = registryURL + "/" + ref } - toolRefs, err := tools.ResolveToolReferences(ctx, h.gptClient, name, entry.Reference, true, toolType) + toolRefs, err := h.resolveToolReferences(ctx, name, entry.Reference, toolType) if err != nil { log.Errorf("Failed to resolve tool references for %s: %v", entry.Reference, err) continue @@ -113,6 +116,39 @@ func (h *Handler) toolsToToolReferences(ctx context.Context, toolType v1.ToolRef return } +func (h *Handler) resolveToolReferences(ctx context.Context, name, reference string, toolType v1.ToolReferenceType) ([]*v1.ToolReference, error) { + annotations := map[string]string{ + "obot.obot.ai/timestamp": time.Now().String(), + } + + var result []*v1.ToolReference + + prg, err := h.gptClient.LoadFile(ctx, reference) + if err != nil { + return nil, err + } + + tool := prg.ToolSet[prg.EntryToolID] + + entryTool := v1.ToolReference{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: system.DefaultNamespace, + Finalizers: []string{v1.ToolReferenceFinalizer}, + Annotations: annotations, + }, + Spec: v1.ToolReferenceSpec{ + Type: toolType, + ToolMetadata: tool.MetaData, + Reference: reference, + Builtin: true, + }, + } + result = append(result, &entryTool) + + return result, nil +} + func (h *Handler) readRegistry(ctx context.Context, registryURL string) (index, error) { run, err := h.gptClient.Run(ctx, registryURL, gptscript.Options{}) if err != nil { @@ -283,30 +319,28 @@ func (h *Handler) ensureModelProviderCredAndDefaults(ctx context.Context, c clie } } - if cred, err := h.gptClient.RevealCredential(ctx, []string{string(modelProviderRef.UID), system.GenericModelProviderCredentialContext}, modelProviderName); err != nil { - if !errors.As(err, &gptscript.ErrNotFound{}) { + if cred, err := h.gatewayClient.RevealCredential(ctx, []string{string(modelProviderRef.UID), system.GenericModelProviderCredentialContext}, modelProviderName); err != nil { + if !errors.As(err, &gateway.CredentialNotFoundError{}) { return fmt.Errorf("failed to check OpenAI credential: %w", err) } // The credential doesn't exist, so create it. - if err = h.gptClient.CreateCredential(ctx, gptscript.Credential{ - Context: string(modelProviderRef.UID), - ToolName: modelProviderName, - Type: gptscript.CredentialTypeModelProvider, - Env: map[string]string{ + if err = h.gatewayClient.UpsertCredential(ctx, gatewaytypes.Credential{ + Context: string(modelProviderRef.UID), + Name: modelProviderName, + Secrets: map[string]string{ credentialEnvVarName: apiKey, }, }); err != nil { return err } log.Infof("Created model provider credential from environment configuration: provider=%s envVar=%s", modelProviderName, envVarName) - } else if cred.Env[credentialEnvVarName] != apiKey { + } else if cred.Secrets[credentialEnvVarName] != apiKey { // If the credential exists, but has a different value, then update it. - if err = h.gptClient.CreateCredential(ctx, gptscript.Credential{ - Context: string(modelProviderRef.UID), - ToolName: modelProviderName, - Type: gptscript.CredentialTypeModelProvider, - Env: map[string]string{ + if err = h.gatewayClient.UpsertCredential(ctx, gatewaytypes.Credential{ + Context: string(modelProviderRef.UID), + Name: modelProviderName, + Secrets: map[string]string{ credentialEnvVarName: apiKey, }, }); err != nil { @@ -372,15 +406,15 @@ func (h *Handler) BackPopulateModels(req router.Request, _ router.Response) erro return err } if len(mps.RequiredConfigurationParameters) > 0 { - cred, err := h.gptClient.RevealCredential(req.Ctx, []string{string(toolRef.UID), system.GenericModelProviderCredentialContext}, toolRef.Name) + cred, err := h.gatewayClient.RevealCredential(req.Ctx, []string{string(toolRef.UID), system.GenericModelProviderCredentialContext}, toolRef.Name) if err != nil { - if errors.As(err, &gptscript.ErrNotFound{}) { + if errors.As(err, &gateway.CredentialNotFoundError{}) { // Unable to find credential, ensure all models remove for this model provider return removeModelsForProvider(req.Ctx, req.Client, req.Namespace, req.Name) } return err } - mps, err = providers.ConvertModelProviderToolRef(*toolRef, cred.Env) + mps, err = providers.ConvertModelProviderToolRef(*toolRef, cred.Secrets) if err != nil { return err } @@ -398,7 +432,7 @@ func (h *Handler) BackPopulateModels(req router.Request, _ router.Response) erro } } - availableModels, err := h.dispatcher.ModelsForProvider(req.Ctx, h.gptClient, req.Namespace, req.Name) + availableModels, err := h.dispatcher.ModelsForProvider(req.Ctx, req.Namespace, req.Name) if err != nil { // Don't error and retry because it will likely fail again. Log the error, and the user can re-sync manually. // Also, the toolRef.Status.Error field will bubble up to the user in the UI. @@ -507,10 +541,13 @@ func (h *Handler) CleanupModelProvider(req router.Request, _ router.Response) er } if len(mps.RequiredConfigurationParameters) > 0 { - if err := h.gptClient.DeleteCredential(req.Ctx, string(toolRef.UID), toolRef.Name); err != nil && !errors.As(err, &gptscript.ErrNotFound{}) { + deleted, err := h.gatewayClient.DeleteCredential(req.Ctx, string(toolRef.UID), toolRef.Name) + if err != nil { return err } - log.Infof("Removed model provider credential during cleanup: provider=%s", toolRef.Name) + if deleted { + log.Infof("Removed model provider credential during cleanup: provider=%s", toolRef.Name) + } } return removeModelsForProvider(req.Ctx, req.Client, req.Namespace, req.Name) diff --git a/pkg/controller/migrate.go b/pkg/controller/migrate.go index 864878c1f3..2c5db0c318 100644 --- a/pkg/controller/migrate.go +++ b/pkg/controller/migrate.go @@ -6,8 +6,9 @@ import ( "fmt" "maps" - "github.com/gptscript-ai/go-gptscript" "github.com/obot-platform/obot/apiclient/types" + gateway "github.com/obot-platform/obot/pkg/gateway/client" + gatewaytypes "github.com/obot-platform/obot/pkg/gateway/types" v1 "github.com/obot-platform/obot/pkg/storage/apis/obot.obot.ai/v1" "github.com/obot-platform/obot/pkg/system" kclient "sigs.k8s.io/controller-runtime/pkg/client" @@ -72,7 +73,7 @@ func migratePublishedArtifactVisibility(ctx context.Context, client kclient.Clie return nil } -func migrateMultiUserMCPServerManifestValuesToCredentials(ctx context.Context, client kclient.Client, gptClient *gptscript.GPTScript) error { +func migrateMultiUserMCPServerManifestValuesToCredentials(ctx context.Context, client kclient.Client, gatewayClient *gateway.Client) error { var servers v1.MCPServerList if err := client.List(ctx, &servers); err != nil { return err @@ -91,19 +92,18 @@ func migrateMultiUserMCPServerManifestValuesToCredentials(ctx context.Context, c } if len(configValues) > 0 { - if existingCred, err := gptClient.RevealCredential(ctx, []string{credCtx}, server.Name); err != nil && !errors.As(err, &gptscript.ErrNotFound{}) { + if existingCred, err := gatewayClient.RevealCredential(ctx, []string{credCtx}, server.Name); err != nil && !errors.As(err, &gateway.CredentialNotFoundError{}) { return fmt.Errorf("failed to find credential for MCP server %s: %w", server.Name, err) } else if err == nil { // Copy the new config values into the existing credential values so we don't lose any existing values that aren't in the manifest. - maps.Copy(existingCred.Env, configValues) - configValues = existingCred.Env + maps.Copy(existingCred.Secrets, configValues) + configValues = existingCred.Secrets } - if err := gptClient.CreateCredential(ctx, gptscript.Credential{ - Context: credCtx, - ToolName: server.Name, - Type: gptscript.CredentialTypeTool, - Env: configValues, + if err := gatewayClient.UpsertCredential(ctx, gatewaytypes.Credential{ + Context: credCtx, + Name: server.Name, + Secrets: configValues, }); err != nil { return fmt.Errorf("failed to create credential for MCP server %s: %w", server.Name, err) } diff --git a/pkg/controller/routes.go b/pkg/controller/routes.go index 7c6647d069..edd5f42a25 100644 --- a/pkg/controller/routes.go +++ b/pkg/controller/routes.go @@ -31,27 +31,27 @@ import ( func (c *Controller) setupRoutes() { root := c.router - toolRef := toolreference.New(c.services.GPTClient, c.services.ProviderDispatcher, c.services.ToolRegistryURLs) + toolRef := toolreference.New(c.services.GPTClient, c.services.GatewayClient, c.services.ProviderDispatcher, c.services.ToolRegistryURLs) threads := threads.NewHandler() - credentialCleanup := cleanup.NewCredentials(c.services.GPTClient, c.services.MCPLoader, c.services.GatewayClient, c.services.ServerURL, c.services.InternalServerURL) + credentialCleanup := cleanup.NewCredentials(c.services.MCPLoader, c.services.GatewayClient, c.services.ServerURL, c.services.InternalServerURL) userCleanup := cleanup.NewUserCleanup(c.services.GatewayClient, c.services.AccessControlRuleHelper) - mcpCatalog := mcpcatalog.New(c.services.DefaultMCPCatalogPath, c.services.DefaultSystemMCPCatalogPath, c.services.GPTClient, c.services.GatewayClient, c.services.AccessControlRuleHelper, c.services.MCPRuntimeBackend) + mcpCatalog := mcpcatalog.New(c.services.DefaultMCPCatalogPath, c.services.DefaultSystemMCPCatalogPath, c.services.GatewayClient, c.services.AccessControlRuleHelper, c.services.MCPRuntimeBackend) skillRepository := skillrepository.New() - mcpserver := mcpserver.New(c.services.GPTClient, c.services.MCPLoader, c.services.MCPNetworkPolicyEnabled, c.services.MCPDefaultDenyAllEgress, c.services.SingleUserIdleServerShutdownInterval, c.services.MultiUserIdleServerShutdownInterval, c.services.AgentIdleServerShutdownInterval, c.services.ServerURL, c.services.MCPRuntimeBackend, c.services.MCPImagePullSecrets) + mcpserver := mcpserver.New(c.services.GatewayClient, c.services.MCPLoader, c.services.MCPNetworkPolicyEnabled, c.services.MCPDefaultDenyAllEgress, c.services.SingleUserIdleServerShutdownInterval, c.services.MultiUserIdleServerShutdownInterval, c.services.AgentIdleServerShutdownInterval, c.services.ServerURL, c.services.MCPRuntimeBackend, c.services.MCPImagePullSecrets) mcpserverinstance := mcpserverinstance.New(c.services.GatewayClient) accesscontrolrule := accesscontrolrule.New(c.services.AccessControlRuleHelper) - mcpWebhookValidations := mcpwebhookvalidation.New(c.services.GPTClient, c.services.MCPHTTPWebhookBaseImage) + mcpWebhookValidations := mcpwebhookvalidation.New(c.services.GatewayClient, c.services.MCPHTTPWebhookBaseImage) powerUserWorkspaceHandler := poweruserworkspace.NewHandler(c.services.GatewayClient) adminWorkspaceHandler := adminworkspace.New(c.services.GatewayClient) - mcpServerCatalogEntryHandler := mcpservercatalogentry.NewHandler(c.services.GPTClient) - auditLogExportHandler := auditlogexport.NewHandler(c.services.GPTClient, c.services.GatewayClient) + mcpServerCatalogEntryHandler := mcpservercatalogentry.NewHandler(c.services.GatewayClient) + auditLogExportHandler := auditlogexport.NewHandler(c.services.GatewayClient) scheduledAuditLogExportHandler := scheduledauditlogexport.NewHandler() - oauthclients := oauthclients.NewHandler(c.services.GPTClient) - systemMCPServerHandler := systemmcpserver.New(c.services.GPTClient, c.services.MCPLoader, c.services.ServerURL) - nanobotAgentHandler := nanobotagent.New(c.services.GPTClient, c.services.PersistentTokenServer, c.services.GatewayClient, c.localK8sRouter, c.services.NanobotAgentImage, c.services.ServerURL, c.services.MCPServerNamespace, c.services.MCPLoader) + oauthclients := oauthclients.NewHandler(c.services.GatewayClient) + systemMCPServerHandler := systemmcpserver.New(c.services.GatewayClient, c.services.MCPLoader, c.services.ServerURL) + nanobotAgentHandler := nanobotagent.New(c.services.PersistentTokenServer, c.services.GatewayClient, c.localK8sRouter, c.services.NanobotAgentImage, c.services.ServerURL, c.services.MCPServerNamespace, c.services.MCPLoader) oktaGroupMigrationHandler := oktagroupmigration.New() projectHandler := project.New(c.services.GatewayClient) - imagePullSecretHandler := imagepullsecret.New(c.services.GPTClient, c.runtimeClient, c.services.MCPRuntimeBackend, c.services.MCPServerNamespace, c.services.ServiceNamespace, c.services.ServiceAccountName, c.services.MCPImagePullSecrets, c.services.ServiceAccountIssuerURL) + imagePullSecretHandler := imagepullsecret.New(c.services.GatewayClient, c.runtimeClient, c.services.MCPRuntimeBackend, c.services.MCPServerNamespace, c.services.ServiceNamespace, c.services.ServiceAccountName, c.services.MCPImagePullSecrets, c.services.ServiceAccountIssuerURL) // Threads root.Type(&v1.Thread{}).HandlerFunc(threads.CleanupOldThreads) diff --git a/pkg/credstores/credstore.go b/pkg/credstores/credstore.go index 42e3ac2090..f35826e3b7 100644 --- a/pkg/credstores/credstore.go +++ b/pkg/credstores/credstore.go @@ -33,18 +33,11 @@ func setUpPostgres(toolRegistries []string, dsn, encryptionConfigFile string) (s } func setUpSQLite(toolRegistries []string, dsn, encryptionConfigFile string) (string, []string, error) { - dbFile, ok := strings.CutPrefix(dsn, "sqlite://file:") - if !ok { - return "", nil, fmt.Errorf("invalid sqlite dsn, must start with sqlite://file: %s", dsn) - } - dbFile, _, _ = strings.Cut(dbFile, "?") - - if !strings.HasSuffix(dbFile, ".db") { - return "", nil, fmt.Errorf("invalid sqlite dsn, file must end in .db: %s", dsn) + dbFile, err := GPTScriptSQLiteFile(dsn) + if err != nil { + return "", nil, err } - dbFile = strings.TrimSuffix(dbFile, ".db") + "-credentials.db" - toolRef, err := resolveToolRef(toolRegistries, "credential-stores/sqlite") if err != nil { return "", nil, err @@ -56,6 +49,20 @@ func setUpSQLite(toolRegistries []string, dsn, encryptionConfigFile string) (str }, nil } +func GPTScriptSQLiteFile(dsn string) (string, error) { + dbFile, ok := strings.CutPrefix(dsn, "sqlite://file:") + if !ok { + return "", fmt.Errorf("invalid sqlite dsn, must start with sqlite://file: %s", dsn) + } + dbFile, _, _ = strings.Cut(dbFile, "?") + + if !strings.HasSuffix(dbFile, ".db") { + return "", fmt.Errorf("invalid sqlite dsn, file must end in .db: %s", dsn) + } + + return strings.TrimSuffix(dbFile, ".db") + "-credentials.db", nil +} + func resolveToolRef(toolRegistries []string, relToolPath string) (string, error) { for _, toolRegistry := range toolRegistries { if remapped := loader.Remap[toolRegistry]; remapped != "" { diff --git a/pkg/gateway/client/credential.go b/pkg/gateway/client/credential.go new file mode 100644 index 0000000000..6e9bb39518 --- /dev/null +++ b/pkg/gateway/client/credential.go @@ -0,0 +1,196 @@ +package client + +import ( + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + + "github.com/obot-platform/obot/pkg/gateway/types" + "gorm.io/gorm" + "gorm.io/gorm/clause" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/storage/value" +) + +var credentialGroupResource = schema.GroupResource{ + Group: "obot.obot.ai", + Resource: "credentials", +} + +const credentialEncryptedSecretsKey = "_obot_encrypted_env" + +type ListCredentialsOptions struct { + CredentialContexts []string + AllContexts bool +} + +// ListCredentials returns the credentials in the given context. +// If AllContexts is true, CredentialContexts is ignored and credentials from all contexts are returned. +// The secrets in the returned credentials are blanked out for security; use RevealCredential to get the secrets for a specific credential. +func (c *Client) ListCredentials(ctx context.Context, opts ListCredentialsOptions) ([]types.Credential, error) { + var credentials []types.Credential + if len(opts.CredentialContexts) == 0 && !opts.AllContexts { + return credentials, nil + } + + db := c.db.WithContext(ctx) + if !opts.AllContexts { + db = db.Where("context IN ?", opts.CredentialContexts) + } + + if err := db.Find(&credentials).Error; err != nil { + return nil, fmt.Errorf("failed to list credentials: %w", err) + } + + for i := range credentials { + if err := c.decryptCredential(ctx, &credentials[i]); err != nil { + return nil, fmt.Errorf("failed to decrypt credential: %w", err) + } + credentials[i].Secrets = blankCredentialSecrets(credentials[i].Secrets) + } + + return credentials, nil +} + +type CredentialNotFoundError struct { + Contexts []string + Name string +} + +func (e CredentialNotFoundError) Unwrap() error { + // This allows errors.Is(err, gorm.ErrRecordNotFound) to work for CredentialNotFoundError. + return gorm.ErrRecordNotFound +} + +func (e CredentialNotFoundError) Error() string { + return fmt.Sprintf("credential not found: contexts=%v, name=%s", e.Contexts, e.Name) +} + +// RevealCredential returns the first credential matching name in the ordered list of contexts. +func (c *Client) RevealCredential(ctx context.Context, contexts []string, name string) (types.Credential, error) { + var credential types.Credential + if len(contexts) == 0 { + return credential, CredentialNotFoundError{Contexts: contexts, Name: name} + } + + for _, credentialContext := range contexts { + if err := c.db.WithContext(ctx).Where("context = ? AND name = ?", credentialContext, name).First(&credential).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + continue + } + return credential, err + } + if err := c.decryptCredential(ctx, &credential); err != nil { + return credential, fmt.Errorf("failed to decrypt credential: %w", err) + } + return credential, nil + } + + return credential, CredentialNotFoundError{Contexts: contexts, Name: name} +} + +// UpsertCredential creates or replaces a credential identified by context+name. +func (c *Client) UpsertCredential(ctx context.Context, credential types.Credential) error { + if credential.Context == "" || credential.Name == "" { + return fmt.Errorf("credential context and name are required") + } + if credential.Secrets == nil { + credential.Secrets = map[string]string{} + } + credential.Encrypted = false + if err := c.encryptCredential(ctx, &credential); err != nil { + return fmt.Errorf("failed to encrypt credential: %w", err) + } + + return c.db.WithContext(ctx).Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "context"}, {Name: "name"}}, + DoUpdates: clause.AssignmentColumns([]string{"secrets", "encrypted"}), + }).Create(&credential).Error +} + +// DeleteCredential deletes a credential if it exists and returns whether a credential was deleted. +func (c *Client) DeleteCredential(ctx context.Context, context, name string) (bool, error) { + result := c.db.WithContext(ctx).Where("context = ? AND name = ?", context, name).Delete(&types.Credential{}) + if result.Error != nil { + return false, fmt.Errorf("failed to delete credential: %w", result.Error) + } + return result.RowsAffected > 0, nil +} + +func (c *Client) encryptCredential(ctx context.Context, credential *types.Credential) error { + if c.encryptionConfig == nil { + return nil + } + + transformer := c.encryptionConfig.Transformers[credentialGroupResource] + if transformer == nil { + return nil + } + + secretsJSON, err := json.Marshal(credential.Secrets) + if err != nil { + return fmt.Errorf("failed to marshal secrets: %w", err) + } + + b, err := transformer.TransformToStorage(ctx, secretsJSON, credentialDataCtx(credential)) + if err != nil { + return err + } + + credential.Secrets = map[string]string{ + credentialEncryptedSecretsKey: base64.StdEncoding.EncodeToString(b), + } + credential.Encrypted = true + return nil +} + +func (c *Client) decryptCredential(ctx context.Context, credential *types.Credential) error { + if !credential.Encrypted || len(credential.Secrets) != 1 || c.encryptionConfig == nil { + return nil + } + + transformer := c.encryptionConfig.Transformers[credentialGroupResource] + if transformer == nil { + return nil + } + + encryptedSecrets := credential.Secrets[credentialEncryptedSecretsKey] + if encryptedSecrets == "" { + return fmt.Errorf("encrypted secrets is missing") + } + + decoded, err := base64.StdEncoding.DecodeString(encryptedSecrets) + if err != nil { + return fmt.Errorf("failed to decode encrypted secrets: %w", err) + } + + out, _, err := transformer.TransformFromStorage(ctx, decoded, credentialDataCtx(credential)) + if err != nil { + return err + } + + var secrets map[string]string + if err := json.Unmarshal(out, &secrets); err != nil { + return fmt.Errorf("failed to unmarshal secrets: %w", err) + } + + credential.Secrets = secrets + return nil +} + +func credentialDataCtx(credential *types.Credential) value.Context { + return value.DefaultContext(fmt.Sprintf("%s///%s", credential.Name, credential.Context)) +} + +func blankCredentialSecrets(secrets map[string]string) map[string]string { + if len(secrets) == 0 { + return secrets + } + blank := make(map[string]string, len(secrets)) + for key := range secrets { + blank[key] = "" + } + return blank +} diff --git a/pkg/gateway/client/credential_migration.go b/pkg/gateway/client/credential_migration.go new file mode 100644 index 0000000000..9dc16f84d6 --- /dev/null +++ b/pkg/gateway/client/credential_migration.go @@ -0,0 +1,131 @@ +package client + +import ( + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "strings" + "time" + + gatewaytypes "github.com/obot-platform/obot/pkg/gateway/types" + "gorm.io/gorm" +) + +const gptscriptCredentialsMigrationName = "gptscript_credentials_to_gateway_credentials" + +type gptscriptCredentialSecret struct { + Env map[string]string `json:"env"` +} + +type gptscriptCredential struct { + ID uint `gorm:"primary_key"` + CreatedAt time.Time + ServerURL string `gorm:"unique"` + Username string + Secret string +} + +// MigrateGPTScriptCredentials migrates existing GPTScript credentials into the +// gateway credentials table. The old GPTScript id, server_url, and username +// fields are intentionally not copied; server_url only supplies the new name. +func (c *Client) MigrateGPTScriptCredentials(ctx context.Context, oldDB *gorm.DB) error { + if oldDB == nil { + return nil + } + + var migration gatewaytypes.Migration + if err := c.db.WithContext(ctx).Where("name = ?", gptscriptCredentialsMigrationName).First(&migration).Error; err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + } else { + return nil + } + + if !oldDB.Migrator().HasTable(gptscriptCredential{}) { + return c.db.WithContext(ctx).Create(&gatewaytypes.Migration{Name: gptscriptCredentialsMigrationName}).Error + } + + var oldCredentials []gptscriptCredential + if err := oldDB.WithContext(ctx).Find(&oldCredentials).Error; err != nil { + return fmt.Errorf("failed to list GPTScript credentials: %w", err) + } + + for _, oldCredential := range oldCredentials { + name, context, _ := strings.Cut(oldCredential.ServerURL, "///") + if name == "" || context == "" { + continue + } + + env, err := c.gptscriptCredentialEnv(ctx, context, name, oldCredential.Secret) + if err != nil { + return fmt.Errorf("failed to decode GPTScript credential %q in context %q: %w", name, context, err) + } + + if err := c.UpsertCredential(ctx, gatewaytypes.Credential{ + Context: context, + Name: name, + Secrets: env, + }); err != nil { + return fmt.Errorf("failed to migrate GPTScript credential %q in context %q: %w", name, context, err) + } + } + + if err := oldDB.Migrator().DropTable(gptscriptCredential{}); err != nil { + return fmt.Errorf("failed to drop old GPTScript credentials table: %w", err) + } + + return c.db.WithContext(ctx).Create(&gatewaytypes.Migration{Name: gptscriptCredentialsMigrationName}).Error +} + +func (c *Client) gptscriptCredentialEnv(ctx context.Context, credentialContext, name, secret string) (map[string]string, error) { + var secretMap map[string]json.RawMessage + if err := json.Unmarshal([]byte(secret), &secretMap); err != nil { + return nil, fmt.Errorf("failed to unmarshal secret: %w", err) + } + + secretBytes := []byte(secret) + if len(secretMap) == 1 { + if encryptedSecret, ok := secretMap["e"]; ok { + if c.encryptionConfig == nil { + return nil, fmt.Errorf("secret is encrypted but encryption is not configured") + } + + transformer := c.encryptionConfig.Transformers[credentialGroupResource] + if transformer == nil { + return nil, fmt.Errorf("secret is encrypted but no credential transformer is configured") + } + + var encoded string + if err := json.Unmarshal(encryptedSecret, &encoded); err != nil { + return nil, fmt.Errorf("failed to unmarshal encrypted secret: %w", err) + } + + decoded, err := base64.StdEncoding.DecodeString(encoded) + if err != nil { + return nil, fmt.Errorf("failed to decode encrypted secret: %w", err) + } + + out, _, err := transformer.TransformFromStorage(ctx, decoded, credentialDataCtx(&gatewaytypes.Credential{ + Context: credentialContext, + Name: name, + })) + if err != nil { + return nil, fmt.Errorf("failed to decrypt secret: %w", err) + } + secretBytes = out + } + } + + var credentialSecret gptscriptCredentialSecret + if err := json.Unmarshal(secretBytes, &credentialSecret); err != nil { + return nil, fmt.Errorf("failed to unmarshal credential env: %w", err) + } + if credentialSecret.Env == nil { + credentialSecret.Env = map[string]string{} + } + + return credentialSecret.Env, nil +} diff --git a/pkg/gateway/db/db.go b/pkg/gateway/db/db.go index f36bb6ab49..53df6695e5 100644 --- a/pkg/gateway/db/db.go +++ b/pkg/gateway/db/db.go @@ -115,6 +115,7 @@ func (db *DB) AutoMigrate() (err error) { types.DeviceScanPlugin{}, types.DeviceScanFile{}, types.DeviceScanClient{}, + types.Credential{}, ); err != nil { return fmt.Errorf("failed to auto migrate gateway types: %w", err) } diff --git a/pkg/gateway/server/dispatcher/availablemodels.go b/pkg/gateway/server/dispatcher/availablemodels.go index 2b414386e4..6c60e6f402 100644 --- a/pkg/gateway/server/dispatcher/availablemodels.go +++ b/pkg/gateway/server/dispatcher/availablemodels.go @@ -8,15 +8,10 @@ import ( "net/http" openai "github.com/gptscript-ai/chat-completion-client" - "github.com/gptscript-ai/go-gptscript" ) -func (d *Dispatcher) ModelsForProvider(ctx context.Context, gptClient *gptscript.GPTScript, modelProviderNamespace, modelProviderName string) (*openai.ModelsList, error) { - return d.ModelsForProviderWithEnv(ctx, gptClient, modelProviderNamespace, modelProviderName, nil) -} - -func (d *Dispatcher) ModelsForProviderWithEnv(ctx context.Context, gptClient *gptscript.GPTScript, modelProviderNamespace, modelProviderName string, env map[string]string) (*openai.ModelsList, error) { - u, err := d.URLForModelProvider(ctx, gptClient, modelProviderNamespace, modelProviderName) +func (d *Dispatcher) ModelsForProvider(ctx context.Context, modelProviderNamespace, modelProviderName string) (*openai.ModelsList, error) { + u, err := d.URLForModelProvider(ctx, modelProviderNamespace, modelProviderName) if err != nil { return nil, fmt.Errorf("failed to get URL for model provider %q: %w", modelProviderName, err) } @@ -29,8 +24,6 @@ func (d *Dispatcher) ModelsForProviderWithEnv(ctx context.Context, gptClient *gp return nil, fmt.Errorf("failed to create request to model provider %q: %w", modelProviderName, err) } - addCredHeaders(r, env) - resp, err := http.DefaultClient.Do(r) if err != nil { return nil, fmt.Errorf("failed to make request to model provider %q: %w", modelProviderName, err) diff --git a/pkg/gateway/server/dispatcher/dispatcher.go b/pkg/gateway/server/dispatcher/dispatcher.go index 70bad2a77a..79cb1e0271 100644 --- a/pkg/gateway/server/dispatcher/dispatcher.go +++ b/pkg/gateway/server/dispatcher/dispatcher.go @@ -9,7 +9,6 @@ import ( "strings" "sync" - "github.com/gptscript-ai/go-gptscript" "github.com/gptscript-ai/gptscript/pkg/engine" "github.com/obot-platform/obot/logger" "github.com/obot-platform/obot/pkg/api/handlers/providers" @@ -26,7 +25,6 @@ import ( type Dispatcher struct { invoker *invoke.Invoker client kclient.Client - gptscriptClient *gptscript.GPTScript gatewayClient *client.Client authLock *sync.RWMutex authURLs map[string]url.URL @@ -35,16 +33,15 @@ type Dispatcher struct { modelURLs map[string]url.URL } -func New(invoker *invoke.Invoker, c kclient.Client, gptscriptClient *gptscript.GPTScript, gatewayClient *client.Client, postgresDSN string) *Dispatcher { +func New(invoker *invoke.Invoker, c kclient.Client, gatewayClient *client.Client, postgresDSN string) *Dispatcher { d := &Dispatcher{ - invoker: invoker, - client: c, - gatewayClient: gatewayClient, - gptscriptClient: gptscriptClient, - modelLock: new(sync.RWMutex), - modelURLs: make(map[string]url.URL), - authLock: new(sync.RWMutex), - authURLs: make(map[string]url.URL), + invoker: invoker, + client: c, + gatewayClient: gatewayClient, + modelLock: new(sync.RWMutex), + modelURLs: make(map[string]url.URL), + authLock: new(sync.RWMutex), + authURLs: make(map[string]url.URL), } if postgresDSN != "" { @@ -54,16 +51,16 @@ func New(invoker *invoke.Invoker, c kclient.Client, gptscriptClient *gptscript.G return d } -func (d *Dispatcher) URLForAuthProvider(ctx context.Context, gptClient *gptscript.GPTScript, namespace, authProviderName string) (url.URL, error) { - u, err := d.urlForProvider(ctx, gptClient, v1.ToolReferenceTypeAuthProvider, namespace, authProviderName, d.authURLs, d.authLock, d.authProviderExtraEnv...) +func (d *Dispatcher) URLForAuthProvider(ctx context.Context, namespace, authProviderName string) (url.URL, error) { + u, err := d.urlForProvider(ctx, v1.ToolReferenceTypeAuthProvider, namespace, authProviderName, d.authURLs, d.authLock, d.authProviderExtraEnv...) if err != nil { return url.URL{}, fmt.Errorf("failed to get auth provider url: %w", err) } return u, nil } -func (d *Dispatcher) URLForModelProvider(ctx context.Context, gptClient *gptscript.GPTScript, namespace, modelProviderName string) (url.URL, error) { - u, err := d.urlForProvider(ctx, gptClient, v1.ToolReferenceTypeModelProvider, namespace, modelProviderName, d.modelURLs, d.modelLock, modelProviderLogLevelEnv()) +func (d *Dispatcher) URLForModelProvider(ctx context.Context, namespace, modelProviderName string) (url.URL, error) { + u, err := d.urlForProvider(ctx, v1.ToolReferenceTypeModelProvider, namespace, modelProviderName, d.modelURLs, d.modelLock, modelProviderLogLevelEnv()) if err != nil { return url.URL{}, fmt.Errorf("failed to get model provider url: %w", err) } @@ -82,7 +79,7 @@ var providerTypeToGenericCredContext = map[v1.ToolReferenceType]string{ v1.ToolReferenceTypeAuthProvider: system.GenericAuthProviderCredentialContext, } -func (d *Dispatcher) urlForProvider(ctx context.Context, gptClient *gptscript.GPTScript, providerType v1.ToolReferenceType, namespace, name string, urlMap map[string]url.URL, lock *sync.RWMutex, extraEnv ...string) (url.URL, error) { +func (d *Dispatcher) urlForProvider(ctx context.Context, providerType v1.ToolReferenceType, namespace, name string, urlMap map[string]url.URL, lock *sync.RWMutex, extraEnv ...string) (url.URL, error) { key := namespace + "/" + name // Check the map with the read lock. lock.RLock() @@ -103,7 +100,7 @@ func (d *Dispatcher) urlForProvider(ctx context.Context, gptClient *gptscript.GP } // We didn't find the provider (or the daemon stopped for some reason), so start it and add it to the map. - u, err := d.startProvider(ctx, gptClient, providerType, namespace, name, extraEnv...) + u, err := d.startProvider(ctx, providerType, namespace, name, extraEnv...) if err != nil { return url.URL{}, err } @@ -112,7 +109,7 @@ func (d *Dispatcher) urlForProvider(ctx context.Context, gptClient *gptscript.GP return u, nil } -func (d *Dispatcher) startProvider(ctx context.Context, gptClient *gptscript.GPTScript, providerType v1.ToolReferenceType, namespace, providerName string, extraEnv ...string) (url.URL, error) { +func (d *Dispatcher) startProvider(ctx context.Context, providerType v1.ToolReferenceType, namespace, providerName string, extraEnv ...string) (url.URL, error) { thread := &v1.Thread{ ObjectMeta: metav1.ObjectMeta{ Name: system.SystemThreadPrefix + providerName, @@ -133,7 +130,7 @@ func (d *Dispatcher) startProvider(ctx context.Context, gptClient *gptscript.GPT return url.URL{}, fmt.Errorf("failed to get provider: %w", err) } - task, err := d.invoker.SystemTask(ctx, gptClient, thread, providerName, "", invoke.SystemTaskOptions{ + task, err := d.invoker.SystemTask(ctx, thread, providerName, "", invoke.SystemTaskOptions{ CredentialContextIDs: []string{string(providerToolRef.UID), providerTypeToGenericCredContext[providerType]}, Env: extraEnv, }) @@ -205,7 +202,7 @@ func (d *Dispatcher) GetConfiguredAuthProvider(ctx context.Context) (string, err } for _, authProvider := range authProviders.Items { - if d.isAuthProviderConfigured(ctx, d.gptscriptClient, authProvider) { + if d.isAuthProviderConfigured(ctx, authProvider) { return authProvider.Name, nil } } @@ -216,12 +213,12 @@ func (d *Dispatcher) GetConfiguredAuthProvider(ctx context.Context) (string, err // isAuthProviderConfigured checks an auth provider to see if all of its required environment variables are set. // Errors are ignored and reported as the auth provider is not configured. // Returns: isConfigured (bool) -func (d *Dispatcher) isAuthProviderConfigured(ctx context.Context, gptscriptClient *gptscript.GPTScript, toolRef v1.ToolReference) bool { +func (d *Dispatcher) isAuthProviderConfigured(ctx context.Context, toolRef v1.ToolReference) bool { if toolRef.Status.Tool == nil { return false } - credEnv, err := CredentialEnvForAuthProvider(ctx, gptscriptClient, toolRef) + credEnv, err := CredentialEnvForAuthProvider(ctx, d.gatewayClient, toolRef) if err != nil { return false } @@ -234,34 +231,19 @@ func (d *Dispatcher) isAuthProviderConfigured(ctx context.Context, gptscriptClie return aps.Configured } -// GetAuthProviderConfigEnv returns the value of the specified environment variable from the given auth providers config. -func (d *Dispatcher) GetAuthProviderConfigEnv(ctx context.Context, gptscriptClient *gptscript.GPTScript, namespace, authProviderName, envName string) (string, error) { - var authProvider v1.ToolReference - if err := d.client.Get(ctx, kclient.ObjectKey{Namespace: namespace, Name: authProviderName}, &authProvider); err != nil { - return "", fmt.Errorf("failed to get auth provider: %w", err) - } - - cred, err := credentialEnvForProvider(ctx, gptscriptClient, authProvider, system.GenericAuthProviderCredentialContext) - if err != nil { - return "", fmt.Errorf("failed to reveal credential: %w", err) - } - - return cred[envName], nil -} - -func CredentialEnvForAuthProvider(ctx context.Context, gptClient *gptscript.GPTScript, authProvider v1.ToolReference) (map[string]string, error) { - return credentialEnvForProvider(ctx, gptClient, authProvider, system.GenericAuthProviderCredentialContext) +func CredentialEnvForAuthProvider(ctx context.Context, gatewayClient *client.Client, authProvider v1.ToolReference) (map[string]string, error) { + return credentialEnvForProvider(ctx, gatewayClient, authProvider, system.GenericAuthProviderCredentialContext) } -func CredentialEnvForModelProvider(ctx context.Context, gptClient *gptscript.GPTScript, modelProvider v1.ToolReference) (map[string]string, error) { - return credentialEnvForProvider(ctx, gptClient, modelProvider, system.GenericModelProviderCredentialContext) +func CredentialEnvForModelProvider(ctx context.Context, gatewayClient *client.Client, modelProvider v1.ToolReference) (map[string]string, error) { + return credentialEnvForProvider(ctx, gatewayClient, modelProvider, system.GenericModelProviderCredentialContext) } -func credentialEnvForProvider(ctx context.Context, gptClient *gptscript.GPTScript, provider v1.ToolReference, genericCredentialContext string) (map[string]string, error) { - cred, err := gptClient.RevealCredential(ctx, []string{string(provider.UID), genericCredentialContext}, provider.Name) +func credentialEnvForProvider(ctx context.Context, gatewayClient *client.Client, provider v1.ToolReference, genericCredentialContext string) (map[string]string, error) { + cred, err := gatewayClient.RevealCredential(ctx, []string{string(provider.UID), genericCredentialContext}, provider.Name) if err != nil { return nil, fmt.Errorf("failed to reveal credential: %w", err) } - return cred.Env, nil + return cred.Secrets, nil } diff --git a/pkg/gateway/server/llmproxy.go b/pkg/gateway/server/llmproxy.go index dd9d865c1b..12fe812eb8 100644 --- a/pkg/gateway/server/llmproxy.go +++ b/pkg/gateway/server/llmproxy.go @@ -170,7 +170,7 @@ func (s *Server) dispatchLLMProxy(req api.Context) error { req.Request.Body = io.NopCloser(bytes.NewReader(b)) req.ContentLength = int64(len(b)) - u, err := s.dispatcher.URLForModelProvider(req.Context(), req.GPTClient, token.Namespace, modelProvider) + u, err := s.dispatcher.URLForModelProvider(req.Context(), token.Namespace, modelProvider) if err != nil { return fmt.Errorf("failed to get model provider: %w", err) } @@ -1023,7 +1023,7 @@ func (l *llmProviderProxy) proxy(req api.Context) error { return types2.NewErrHTTP(http.StatusTooManyRequests, fmt.Sprintf("no tokens remaining (prompt tokens remaining: %d, completion tokens remaining: %d)", remainingUsage.PromptTokens, remainingUsage.CompletionTokens)) } - credEnv, err := dispatcher.CredentialEnvForModelProvider(req.Context(), req.GPTClient, *modelProvider) + credEnv, err := dispatcher.CredentialEnvForModelProvider(req.Context(), req.GatewayClient, *modelProvider) if err != nil { return fmt.Errorf("failed to get credential environment for model provider: %w", err) } diff --git a/pkg/gateway/server/tokenreview.go b/pkg/gateway/server/tokenreview.go index de545bfb2b..03417a8248 100644 --- a/pkg/gateway/server/tokenreview.go +++ b/pkg/gateway/server/tokenreview.go @@ -5,7 +5,6 @@ import ( "net/http" "strings" - "github.com/gptscript-ai/go-gptscript" "github.com/obot-platform/obot/pkg/auth" "github.com/obot-platform/obot/pkg/gateway/client" "github.com/obot-platform/obot/pkg/gateway/server/dispatcher" @@ -16,14 +15,12 @@ import ( type gatewayTokenReview struct { gatewayClient *client.Client - gptClient *gptscript.GPTScript dispatcher *dispatcher.Dispatcher } -func NewGatewayTokenReviewer(gatewayClient *client.Client, gptClient *gptscript.GPTScript, dispatcher *dispatcher.Dispatcher) authenticator.Request { +func NewGatewayTokenReviewer(gatewayClient *client.Client, dispatcher *dispatcher.Dispatcher) authenticator.Request { return &gatewayTokenReview{ gatewayClient: gatewayClient, - gptClient: gptClient, dispatcher: dispatcher, } } @@ -44,7 +41,7 @@ func (g *gatewayTokenReview) AuthenticateRequest(req *http.Request) (*authentica return nil, false, err } - if err := populateContext(req, g.gptClient, g.dispatcher, namespace, name); err != nil { + if err := populateContext(req, g.dispatcher, namespace, name); err != nil { return nil, false, err } @@ -62,8 +59,8 @@ func (g *gatewayTokenReview) AuthenticateRequest(req *http.Request) (*authentica }, true, nil } -func populateContext(req *http.Request, gptClient *gptscript.GPTScript, dispatcher *dispatcher.Dispatcher, namespace, name string) error { - providerURL, err := dispatcher.URLForAuthProvider(req.Context(), gptClient, namespace, name) +func populateContext(req *http.Request, dispatcher *dispatcher.Dispatcher, namespace, name string) error { + providerURL, err := dispatcher.URLForAuthProvider(req.Context(), namespace, name) if err != nil { return err } diff --git a/pkg/gateway/server/user.go b/pkg/gateway/server/user.go index 885d605406..59e3de03ff 100644 --- a/pkg/gateway/server/user.go +++ b/pkg/gateway/server/user.go @@ -34,7 +34,7 @@ func (s *Server) getCurrentUser(apiContext api.Context) error { name, namespace := apiContext.AuthProviderNameAndNamespace() if name != "" && namespace != "" { - providerURL, err := s.dispatcher.URLForAuthProvider(apiContext.Context(), apiContext.GPTClient, namespace, name) + providerURL, err := s.dispatcher.URLForAuthProvider(apiContext.Context(), namespace, name) if err != nil { return fmt.Errorf("failed to get auth provider URL: %v", err) } @@ -350,7 +350,7 @@ func (s *Server) listAuthGroups(apiContext api.Context) error { return apiContext.Write([]types.Group{}) } - providerURL, err := s.dispatcher.URLForAuthProvider(apiContext.Context(), apiContext.GPTClient, namespace, name) + providerURL, err := s.dispatcher.URLForAuthProvider(apiContext.Context(), namespace, name) if err != nil { return fmt.Errorf("failed to get auth provider URL: %v", err) } diff --git a/pkg/gateway/types/credentials.go b/pkg/gateway/types/credentials.go new file mode 100644 index 0000000000..c46be2f44d --- /dev/null +++ b/pkg/gateway/types/credentials.go @@ -0,0 +1,14 @@ +package types + +import "time" + +// Credential stores secret environment variables for an Obot resource. +// List operations intentionally return Secrets keys with blank values; use RevealCredential to read values. +type Credential struct { + ID uint `json:"id" gorm:"primaryKey;autoIncrement"` + CreatedAt time.Time `json:"createdAt"` + Context string `json:"context" gorm:"uniqueIndex:idx_credentials_context_name;not null"` + Name string `json:"name" gorm:"uniqueIndex:idx_credentials_context_name;not null"` + Secrets map[string]string `json:"secrets" gorm:"serializer:json"` + Encrypted bool `json:"-"` +} diff --git a/pkg/invoke/invoker.go b/pkg/invoke/invoker.go index 08cd44ef99..bad6962f9a 100644 --- a/pkg/invoke/invoker.go +++ b/pkg/invoke/invoker.go @@ -35,6 +35,7 @@ const ( ) type Invoker struct { + gptClient *gptscript.GPTScript uncached kclient.WithWatch gatewayClient *client.Client tokenService *persistent.TokenService @@ -42,13 +43,14 @@ type Invoker struct { internalServerURL string } -func NewInvoker(c kclient.WithWatch, gatewayClient *client.Client, serverURL string, serverPort int, tokenService *persistent.TokenService) *Invoker { +func NewInvoker(c kclient.WithWatch, gatewayClient *client.Client, serverURL string, serverPort int, tokenService *persistent.TokenService, gptClient *gptscript.GPTScript) *Invoker { return &Invoker{ uncached: c, gatewayClient: gatewayClient, tokenService: tokenService, serverURL: serverURL, internalServerURL: fmt.Sprintf("http://localhost:%d", serverPort), + gptClient: gptClient, } } @@ -235,7 +237,7 @@ func isEphemeral(run *v1.Run) bool { return strings.HasPrefix(run.Name, ephemeralRunPrefix) } -func (i *Invoker) createRun(ctx context.Context, gptClient *gptscript.GPTScript, c kclient.WithWatch, thread *v1.Thread, tool any, input string, opts runOptions) (*Response, error) { +func (i *Invoker) createRun(ctx context.Context, c kclient.WithWatch, thread *v1.Thread, tool any, input string, opts runOptions) (*Response, error) { toolData, err := json.Marshal(tool) if err != nil { return nil, err @@ -273,7 +275,7 @@ func (i *Invoker) createRun(ctx context.Context, gptClient *gptscript.GPTScript, resp.gatewayClient = i.gatewayClient resp.cancel = cancel go func() { - if err := i.Resume(ctx, gptClient, c, thread, &run); err != nil { + if err := i.Resume(ctx, c, thread, &run); err != nil { log.Errorf("run failed: run=%s thread=%s error=%s", run.Name, thread.Name, err) } }() @@ -281,7 +283,7 @@ func (i *Invoker) createRun(ctx context.Context, gptClient *gptscript.GPTScript, return resp, nil } -func (i *Invoker) Resume(ctx context.Context, gptClient *gptscript.GPTScript, c kclient.WithWatch, thread *v1.Thread, run *v1.Run) (err error) { +func (i *Invoker) Resume(ctx context.Context, c kclient.WithWatch, thread *v1.Thread, run *v1.Run) (err error) { input := run.Spec.Input chatState, err := i.getChatState(ctx, c, run) @@ -339,7 +341,7 @@ func (i *Invoker) Resume(ctx context.Context, gptClient *gptscript.GPTScript, c if err != nil { return fmt.Errorf("failed to resolve tool reference: %w", err) } - runResp, err = gptClient.Run(ctx, toolRef, options) + runResp, err = i.gptClient.Run(ctx, toolRef, options) if err != nil { return fmt.Errorf("failed to run tool: %w", err) } @@ -347,7 +349,7 @@ func (i *Invoker) Resume(ctx context.Context, gptClient *gptscript.GPTScript, c if err := json.Unmarshal([]byte(run.Spec.Tool), &toolDefs); err != nil { return fmt.Errorf("invalid tool definition: %s: %w", run.Spec.Tool, err) } - runResp, err = gptClient.Evaluate(ctx, options, toolDefs...) + runResp, err = i.gptClient.Evaluate(ctx, options, toolDefs...) if err != nil { return fmt.Errorf("failed to evaluate tool: %w", err) } @@ -355,7 +357,7 @@ func (i *Invoker) Resume(ctx context.Context, gptClient *gptscript.GPTScript, c if err := json.Unmarshal([]byte(run.Spec.Tool), &toolDef); err != nil { return fmt.Errorf("invalid tool definition: %s: %w", run.Spec.Tool, err) } - runResp, err = gptClient.Evaluate(ctx, options, toolDef) + runResp, err = i.gptClient.Evaluate(ctx, options, toolDef) if err != nil { return fmt.Errorf("failed to evaluate tool: %w", err) } diff --git a/pkg/invoke/system.go b/pkg/invoke/system.go index 67fd450ec3..44ca654bca 100644 --- a/pkg/invoke/system.go +++ b/pkg/invoke/system.go @@ -5,7 +5,6 @@ import ( "encoding/json" "time" - "github.com/gptscript-ai/go-gptscript" v1 "github.com/obot-platform/obot/pkg/storage/apis/obot.obot.ai/v1" ) @@ -49,7 +48,7 @@ func inputToString(input any) (string, error) { return inputString, nil } -func (i *Invoker) SystemTask(ctx context.Context, gptClient *gptscript.GPTScript, thread *v1.Thread, tool, input any, opts ...SystemTaskOptions) (*Response, error) { +func (i *Invoker) SystemTask(ctx context.Context, thread *v1.Thread, tool, input any, opts ...SystemTaskOptions) (*Response, error) { opt := complete(opts) inputString, err := inputToString(input) @@ -67,7 +66,7 @@ func (i *Invoker) SystemTask(ctx context.Context, gptClient *gptscript.GPTScript } credContexts = append(opt.CredentialContextIDs, credContexts...) - return i.createRun(ctx, gptClient, i.uncached, thread, tool, inputString, runOptions{ + return i.createRun(ctx, i.uncached, thread, tool, inputString, runOptions{ Env: opt.Env, CredentialContextIDs: credContexts, Timeout: opt.Timeout, diff --git a/pkg/jwt/persistent/persistent.go b/pkg/jwt/persistent/persistent.go index 311bc20298..5a8e899dfa 100644 --- a/pkg/jwt/persistent/persistent.go +++ b/pkg/jwt/persistent/persistent.go @@ -16,10 +16,10 @@ import ( "github.com/MicahParks/jwkset" "github.com/golang-jwt/jwt/v5" - "github.com/gptscript-ai/go-gptscript" "github.com/obot-platform/obot/logger" "github.com/obot-platform/obot/pkg/api" "github.com/obot-platform/obot/pkg/gateway/client" + gatewaytypes "github.com/obot-platform/obot/pkg/gateway/types" "github.com/obot-platform/obot/pkg/system" "k8s.io/apiserver/pkg/authentication/authenticator" "k8s.io/apiserver/pkg/authentication/user" @@ -28,19 +28,17 @@ import ( var log = logger.Package() type TokenService struct { - lock sync.RWMutex - privateKey ed25519.PrivateKey - jwks json.RawMessage - gatewayClient *client.Client - credOnlyGPTClient *gptscript.GPTScript - serverURL string + lock sync.RWMutex + privateKey ed25519.PrivateKey + jwks json.RawMessage + gatewayClient *client.Client + serverURL string } -func NewTokenService(serverURL string, gatewayClient *client.Client, credOnlyGPTClient *gptscript.GPTScript) (*TokenService, error) { +func NewTokenService(serverURL string, gatewayClient *client.Client) (*TokenService, error) { t := &TokenService{ - gatewayClient: gatewayClient, - credOnlyGPTClient: credOnlyGPTClient, - serverURL: serverURL, + gatewayClient: gatewayClient, + serverURL: serverURL, } return t, nil } @@ -55,13 +53,13 @@ const ( // EnsureJWK ensures that the JWK is created and stored in the GPTScript client. It should only be called in a controller post-start hook which only allows one to be run at a time. func (t *TokenService) EnsureJWK(ctx context.Context) error { // Read the credential, if it exists, then use it. - cred, err := t.credOnlyGPTClient.RevealCredential(ctx, []string{system.JWKCredentialContext}, system.JWKCredentialContext) - if err != nil && !errors.As(err, &gptscript.ErrNotFound{}) { + cred, err := t.gatewayClient.RevealCredential(ctx, []string{system.JWKCredentialContext}, system.JWKCredentialContext) + if err != nil && !errors.As(err, &client.CredentialNotFoundError{}) { return err } var configuredKey ed25519.PrivateKey - if keyData := cred.Env[keyEnvVar]; keyData != "" { + if keyData := cred.Secrets[keyEnvVar]; keyData != "" { configuredKey, err = base64.StdEncoding.DecodeString(keyData) if err != nil { return err @@ -75,11 +73,10 @@ func (t *TokenService) EnsureJWK(ctx context.Context) error { } // Write the key to the JWK Set storage. - if err := t.credOnlyGPTClient.CreateCredential(ctx, gptscript.Credential{ - Context: system.JWKCredentialContext, - ToolName: system.JWKCredentialContext, - Type: gptscript.CredentialTypeTool, - Env: map[string]string{ + if err := t.gatewayClient.UpsertCredential(ctx, gatewaytypes.Credential{ + Context: system.JWKCredentialContext, + Name: system.JWKCredentialContext, + Secrets: map[string]string{ keyEnvVar: base64.StdEncoding.EncodeToString(configuredKey), }, }); err != nil { @@ -91,12 +88,12 @@ func (t *TokenService) EnsureJWK(ctx context.Context) error { // SetJWK sets the JWK in the GPTScript client. It should be called after the JWK is created and stored in the GPTScript client. func (t *TokenService) setJWK(ctx context.Context) error { - cred, err := t.credOnlyGPTClient.RevealCredential(ctx, []string{system.JWKCredentialContext}, system.JWKCredentialContext) - if err != nil && !errors.As(err, &gptscript.ErrNotFound{}) { + cred, err := t.gatewayClient.RevealCredential(ctx, []string{system.JWKCredentialContext}, system.JWKCredentialContext) + if err != nil && !errors.As(err, &client.CredentialNotFoundError{}) { return err } - value, ok := cred.Env[keyEnvVar] + value, ok := cred.Secrets[keyEnvVar] if !ok || value == "" { return fmt.Errorf("JWK not found in credential") } @@ -120,11 +117,10 @@ func (t *TokenService) ReplaceJWK(req api.Context) error { return fmt.Errorf("failed to generate key: %w", err) } - if err := req.GPTClient.CreateCredential(req.Context(), gptscript.Credential{ - Context: system.JWKCredentialContext, - ToolName: system.JWKCredentialContext, - Type: gptscript.CredentialTypeTool, - Env: map[string]string{ + if err := req.GatewayClient.UpsertCredential(req.Context(), gatewaytypes.Credential{ + Context: system.JWKCredentialContext, + Name: system.JWKCredentialContext, + Secrets: map[string]string{ keyEnvVar: base64.StdEncoding.EncodeToString(newKey), }, }); err != nil { diff --git a/pkg/messagepolicy/evaluate.go b/pkg/messagepolicy/evaluate.go index 680e9a0e58..8b757f1814 100644 --- a/pkg/messagepolicy/evaluate.go +++ b/pkg/messagepolicy/evaluate.go @@ -168,7 +168,7 @@ func (h *Helper) resolveModelByAlias(ctx context.Context, aliasType types.Defaul return nil, fmt.Errorf("model %q is not active", model.Spec.Manifest.Name) } - providerURL, err := h.dispatcher.URLForModelProvider(ctx, h.gptClient, system.DefaultNamespace, model.Spec.Manifest.ModelProvider) + providerURL, err := h.dispatcher.URLForModelProvider(ctx, system.DefaultNamespace, model.Spec.Manifest.ModelProvider) if err != nil { return nil, fmt.Errorf("failed to get model provider URL: %w", err) } @@ -178,7 +178,7 @@ func (h *Helper) resolveModelByAlias(ctx context.Context, aliasType types.Defaul return nil, fmt.Errorf("failed to get model provider tool reference: %w", err) } - credEnv, err := dispatcher.CredentialEnvForModelProvider(ctx, h.gptClient, toolRef) + credEnv, err := dispatcher.CredentialEnvForModelProvider(ctx, h.gatewayClient, toolRef) if err != nil { return nil, fmt.Errorf("failed to get model provider credentials: %w", err) } diff --git a/pkg/messagepolicy/helper.go b/pkg/messagepolicy/helper.go index 7e6a9ed131..58eca42803 100644 --- a/pkg/messagepolicy/helper.go +++ b/pkg/messagepolicy/helper.go @@ -4,10 +4,10 @@ import ( "context" "fmt" - "github.com/gptscript-ai/go-gptscript" "github.com/obot-platform/nah/pkg/backend" "github.com/obot-platform/obot/apiclient/types" "github.com/obot-platform/obot/logger" + gateway "github.com/obot-platform/obot/pkg/gateway/client" "github.com/obot-platform/obot/pkg/gateway/server/dispatcher" v1 "github.com/obot-platform/obot/pkg/storage/apis/obot.obot.ai/v1" kuser "k8s.io/apiserver/pkg/authentication/user" @@ -24,13 +24,13 @@ const ( ) type Helper struct { - indexer gocache.Indexer - client kclient.Client - dispatcher *dispatcher.Dispatcher - gptClient *gptscript.GPTScript + indexer gocache.Indexer + client kclient.Client + dispatcher *dispatcher.Dispatcher + gatewayClient *gateway.Client } -func NewHelper(ctx context.Context, backend backend.Backend, client kclient.Client, dispatcher *dispatcher.Dispatcher, gptClient *gptscript.GPTScript) (*Helper, error) { +func NewHelper(ctx context.Context, backend backend.Backend, client kclient.Client, dispatcher *dispatcher.Dispatcher, gatewayClient *gateway.Client) (*Helper, error) { gvk, err := backend.GroupVersionKindFor(&v1.MessagePolicy{}) if err != nil { return nil, err @@ -50,10 +50,10 @@ func NewHelper(ctx context.Context, backend backend.Backend, client kclient.Clie } return &Helper{ - indexer: informer.GetIndexer(), - client: client, - dispatcher: dispatcher, - gptClient: gptClient, + indexer: informer.GetIndexer(), + client: client, + dispatcher: dispatcher, + gatewayClient: gatewayClient, }, nil } diff --git a/pkg/proxy/proxy.go b/pkg/proxy/proxy.go index ac037fa344..1eb66ef14f 100644 --- a/pkg/proxy/proxy.go +++ b/pkg/proxy/proxy.go @@ -12,7 +12,6 @@ import ( "strings" "time" - "github.com/gptscript-ai/go-gptscript" "github.com/obot-platform/obot/logger" "github.com/obot-platform/obot/pkg/accesstoken" "github.com/obot-platform/obot/pkg/api" @@ -36,13 +35,11 @@ var ErrInvalidSession = errors.New("invalid session") type Manager struct { dispatcher *dispatcher.Dispatcher - gptClient *gptscript.GPTScript } -func NewProxyManager(dispatcher *dispatcher.Dispatcher, gptClient *gptscript.GPTScript) *Manager { +func NewProxyManager(dispatcher *dispatcher.Dispatcher) *Manager { m := &Manager{ dispatcher: dispatcher, - gptClient: gptClient, } return m @@ -64,7 +61,7 @@ func (pm *Manager) AuthenticateRequest(req *http.Request) (*authenticator.Respon return nil, false, ErrInvalidSession } - proxy, err := pm.createProxy(req.Context(), pm.gptClient, system.DefaultNamespace+"/"+configuredProvider) + proxy, err := pm.createProxy(req.Context(), system.DefaultNamespace+"/"+configuredProvider) if err != nil { return nil, false, err } @@ -161,7 +158,7 @@ func (pm *Manager) ServeHTTP(user user.Info, w http.ResponseWriter, r *http.Requ } } - proxy, err := pm.createProxy(r.Context(), pm.gptClient, provider) + proxy, err := pm.createProxy(r.Context(), provider) if err != nil { if r.URL.Path != "/oauth2/sign_out" { http.Error(w, fmt.Sprintf("failed to create proxy: %v", err), http.StatusInternalServerError) @@ -189,13 +186,13 @@ func (pm *Manager) ServeHTTP(user user.Info, w http.ResponseWriter, r *http.Requ proxy.serveHTTP(w, r) } -func (pm *Manager) createProxy(ctx context.Context, gptClient *gptscript.GPTScript, provider string) (*Proxy, error) { +func (pm *Manager) createProxy(ctx context.Context, provider string) (*Proxy, error) { parts := strings.Split(provider, "/") if len(parts) != 2 { return nil, fmt.Errorf("invalid provider: %s", provider) } - providerURL, err := pm.dispatcher.URLForAuthProvider(ctx, gptClient, parts[0], parts[1]) + providerURL, err := pm.dispatcher.URLForAuthProvider(ctx, parts[0], parts[1]) if err != nil { return nil, err } diff --git a/pkg/services/config.go b/pkg/services/config.go index 168131cf3f..259ad5b613 100644 --- a/pkg/services/config.go +++ b/pkg/services/config.go @@ -13,6 +13,7 @@ import ( "time" "github.com/adrg/xdg" + "github.com/glebarez/sqlite" "github.com/gptscript-ai/go-gptscript" "github.com/gptscript-ai/gptscript/pkg/cache" gptscriptai "github.com/gptscript-ai/gptscript/pkg/gptscript" @@ -60,6 +61,7 @@ import ( "github.com/obot-platform/obot/pkg/storage/services" "github.com/obot-platform/obot/pkg/system" "gorm.io/gorm" + gormlogger "gorm.io/gorm/logger" coordinationv1 "k8s.io/api/coordination/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" @@ -288,8 +290,8 @@ func copyKeys(envs []string) []string { return newEnvs } -// buildLocalK8sConfig creates a Kubernetes config for local cluster access -func buildLocalK8sConfig() (*rest.Config, error) { +// BuildLocalK8sConfig creates a Kubernetes config for local cluster access +func BuildLocalK8sConfig() (*rest.Config, error) { cfg, err := rest.InClusterConfig() if err == nil { return cfg, nil @@ -301,10 +303,6 @@ func buildLocalK8sConfig() (*rest.Config, error) { return clientcmd.BuildConfigFromFlags("", kubeconfig) } -func BuildLocalK8sConfig() (*rest.Config, error) { - return buildLocalK8sConfig() -} - // unmarshalJSONStrict unmarshals JSON with strict validation that rejects unknown fields func unmarshalJSONStrict(data []byte, v any) error { decoder := json.NewDecoder(bytes.NewReader(data)) @@ -604,6 +602,9 @@ func New(ctx context.Context, config Config) (*Services, error) { config.MCPAuditLogsPersistBatchSize, config.MCPAuditLogRetentionDays, ) + if err := migrateGPTScriptCredentials(ctx, gatewayClient, gatewayDB, config.DSN); err != nil { + return nil, fmt.Errorf("failed to migrate GPTScript credentials: %w", err) + } storageServices.Authn.SetServiceAccountValidator(func(ctx context.Context, token string) (string, error) { apiKey, err := gatewayClient.ValidateStorageServiceAccountToken(ctx, token) if err != nil { @@ -627,7 +628,7 @@ func New(ctx context.Context, config Config) (*Services, error) { serviceAccountIssuerError string ) if mcp.IsKubernetesBackend(config.MCPRuntimeBackend) { - localK8sConfig, err = buildLocalK8sConfig() + localK8sConfig, err = BuildLocalK8sConfig() if err != nil { return nil, fmt.Errorf("failed to build local Kubernetes config: %w", err) } @@ -656,25 +657,11 @@ func New(ctx context.Context, config Config) (*Services, error) { postgresDSN = config.DSN } - credOnlyGPTscriptClient, err := newGPTScript(ctx, config.EnvKeys, credStore, credStoreEnv, nil) - if err != nil { - return nil, err - } - - persistentTokenServer, err := persistent.NewTokenService(config.Hostname, gatewayClient, credOnlyGPTscriptClient) + persistentTokenServer, err := persistent.NewTokenService(config.Hostname, gatewayClient) if err != nil { return nil, fmt.Errorf("failed to setup persistent token service: %w", err) } - invoker := invoke.NewInvoker( - storageClient, - gatewayClient, - config.Hostname, - config.HTTPListenPort, - persistentTokenServer, - ) - providerDispatcher := dispatcher.New(invoker, storageClient, credOnlyGPTscriptClient, gatewayClient, postgresDSN) - r, err := nah.NewRouter("obot-controller", &nah.Options{ RESTConfig: restConfig, Scheme: scheme.Scheme, @@ -900,9 +887,20 @@ func New(ctx context.Context, config Config) (*Services, error) { return nil, err } + invoker := invoke.NewInvoker( + storageClient, + gatewayClient, + config.Hostname, + config.HTTPListenPort, + persistentTokenServer, + gptscriptClient, + ) + + providerDispatcher := dispatcher.New(invoker, storageClient, gatewayClient, postgresDSN) + var msgPolicyHelper *messagepolicy.Helper if config.EnableMessagePolicies { - msgPolicyHelper, err = messagepolicy.NewHelper(ctx, r.Backend(), storageClient, providerDispatcher, credOnlyGPTscriptClient) + msgPolicyHelper, err = messagepolicy.NewHelper(ctx, r.Backend(), storageClient, providerDispatcher, gatewayClient) if err != nil { return nil, err } @@ -912,7 +910,7 @@ func New(ctx context.Context, config Config) (*Services, error) { apply.AddValidOwnerChange("mcpcatalogentries", "catalog-default") var proxyManager *proxy.Manager - bootstrapper, err := bootstrap.New(ctx, config.Hostname, gatewayClient, gptscriptClient, config.EnableAuthentication, config.ForceEnableBootstrap) + bootstrapper, err := bootstrap.New(ctx, config.Hostname, gatewayClient, config.EnableAuthentication, config.ForceEnableBootstrap) if err != nil { return nil, err } @@ -923,9 +921,9 @@ func New(ctx context.Context, config Config) (*Services, error) { return nil, err } - authenticators := gserver.NewGatewayTokenReviewer(gatewayClient, gptscriptClient, providerDispatcher) + authenticators := gserver.NewGatewayTokenReviewer(gatewayClient, providerDispatcher) if config.EnableAuthentication { - proxyManager = proxy.NewProxyManager(providerDispatcher, gptscriptClient) + proxyManager = proxy.NewProxyManager(providerDispatcher) // Token Auth + OAuth auth authenticators = union.NewFailOnError(authenticators, proxyManager) @@ -1025,7 +1023,6 @@ func New(ctx context.Context, config Config) (*Services, error) { APIServer: server.NewServer( storageClient, gatewayClient, - gptscriptClient, apiLocalK8sClient, config.ServiceNamespace, authn.NewAuthenticator(authenticators), @@ -1124,6 +1121,40 @@ func New(ctx context.Context, config Config) (*Services, error) { return svcs, nil } +func migrateGPTScriptCredentials(ctx context.Context, gatewayClient *client.Client, gatewayDB *db.DB, dsn string) error { + if strings.HasPrefix(dsn, "postgres://") { + return gatewayClient.MigrateGPTScriptCredentials(ctx, gatewayDB.WithContext(ctx)) + } + + if !strings.HasPrefix(dsn, "sqlite://") { + return nil + } + + credentialDBFile, err := credstores.GPTScriptSQLiteFile(dsn) + if err != nil { + return err + } + if _, err := os.Stat(credentialDBFile); errors.Is(err, os.ErrNotExist) { + return nil + } else if err != nil { + return fmt.Errorf("failed to stat GPTScript credential database %q: %w", credentialDBFile, err) + } + + oldDB, err := gorm.Open(sqlite.Open(credentialDBFile), &gorm.Config{ + Logger: gormlogger.Default.LogMode(gormlogger.Silent), + }) + if err != nil { + return fmt.Errorf("failed to open GPTScript credential database %q: %w", credentialDBFile, err) + } + sqlDB, err := oldDB.DB() + if err != nil { + return fmt.Errorf("failed to get GPTScript credential database handle: %w", err) + } + defer sqlDB.Close() + + return gatewayClient.MigrateGPTScriptCredentials(ctx, oldDB) +} + func buildArtifactStorageConfig(config Config) apiclienttypes.StorageConfig { switch apiclienttypes.StorageProviderType(config.ArtifactStorageProvider) { case apiclienttypes.StorageProviderS3: diff --git a/pkg/tools/resolve.go b/pkg/tools/resolve.go deleted file mode 100644 index 3ddb717eb0..0000000000 --- a/pkg/tools/resolve.go +++ /dev/null @@ -1,44 +0,0 @@ -package tools - -import ( - "context" - "time" - - "github.com/gptscript-ai/go-gptscript" - v1 "github.com/obot-platform/obot/pkg/storage/apis/obot.obot.ai/v1" - "github.com/obot-platform/obot/pkg/system" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func ResolveToolReferences(ctx context.Context, gptClient *gptscript.GPTScript, name, reference string, builtin bool, toolType v1.ToolReferenceType) ([]*v1.ToolReference, error) { - annotations := map[string]string{ - "obot.obot.ai/timestamp": time.Now().String(), - } - - var result []*v1.ToolReference - - prg, err := gptClient.LoadFile(ctx, reference) - if err != nil { - return nil, err - } - - tool := prg.ToolSet[prg.EntryToolID] - - entryTool := v1.ToolReference{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: system.DefaultNamespace, - Finalizers: []string{v1.ToolReferenceFinalizer}, - Annotations: annotations, - }, - Spec: v1.ToolReferenceSpec{ - Type: toolType, - ToolMetadata: tool.MetaData, - Reference: reference, - Builtin: builtin, - }, - } - result = append(result, &entryTool) - - return result, nil -} From 69844804f0d080f6ba07797cb0ab6a009f6791d3 Mon Sep 17 00:00:00 2001 From: Donnie Adams Date: Thu, 21 May 2026 20:37:06 -0400 Subject: [PATCH 2/2] chore: integrate Obot credstore as GPTScript credstore All of this code will be removed once we no longer need GPTScript for auth and model providers. Signed-off-by: Donnie Adams --- go.mod | 4 +- go.sum | 4 +- pkg/api/authz/authz.go | 8 ++ pkg/api/handlers/credential.go | 205 +++++++++++++++++++++++++++++++++ pkg/api/router/router.go | 9 ++ pkg/credstores/credstore.go | 66 +---------- pkg/encryption/encryption.go | 14 +-- pkg/server/server.go | 38 +++++- pkg/services/config.go | 116 ++++++++++--------- 9 files changed, 337 insertions(+), 127 deletions(-) create mode 100644 pkg/api/handlers/credential.go diff --git a/go.mod b/go.mod index 8e2fa80494..94f5512e97 100644 --- a/go.mod +++ b/go.mod @@ -27,17 +27,17 @@ require ( github.com/containerd/errdefs v1.0.0 github.com/docker/go-connections v0.6.0 github.com/fatih/color v1.18.0 + github.com/glebarez/sqlite v1.11.0 github.com/go-git/go-billy/v5 v5.8.0 github.com/go-git/go-git/v5 v5.17.0 github.com/gobwas/glob v0.2.3 github.com/golang-jwt/jwt/v5 v5.3.0 - github.com/glebarez/sqlite v1.11.0 github.com/google/jsonschema-go v0.4.2 github.com/google/uuid v1.6.0 github.com/gptscript-ai/chat-completion-client v0.0.0-20250224164718-139cb4507b1d github.com/gptscript-ai/cmd v0.0.0-20250530150401-bc71fddf8070 github.com/gptscript-ai/go-gptscript v0.9.9-0.20260205140523-98f64d42d2ee - github.com/gptscript-ai/gptscript v0.9.10-0.20260507143209-6761bb8f98e8 + github.com/gptscript-ai/gptscript v0.9.10-0.20260522151821-b374bab0897c github.com/jackc/pgx/v5 v5.9.1 github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de github.com/moby/moby/api v1.52.0-alpha.1 diff --git a/go.sum b/go.sum index 95fbc563b3..c7449d06da 100644 --- a/go.sum +++ b/go.sum @@ -470,8 +470,8 @@ github.com/gptscript-ai/cmd v0.0.0-20250530150401-bc71fddf8070 h1:xm5ZZFraWFwxyE github.com/gptscript-ai/cmd v0.0.0-20250530150401-bc71fddf8070/go.mod h1:DJAo1xTht1LDkNYFNydVjTHd576TC7MlpsVRl3oloVw= github.com/gptscript-ai/go-gptscript v0.9.9-0.20260205140523-98f64d42d2ee h1:8+510/jOqfLZ7l3c25TMtkKn33PQBEzuNiIt4XbJ7DY= github.com/gptscript-ai/go-gptscript v0.9.9-0.20260205140523-98f64d42d2ee/go.mod h1:8JGZNO+x4tkTrkT1q5PMrVCKY2AcnS/Ru8WCNvzLYIg= -github.com/gptscript-ai/gptscript v0.9.10-0.20260507143209-6761bb8f98e8 h1:3umvmDb2cWxAfFlcw8iQpVtiU0FgsJ0WDuej2QHXJQY= -github.com/gptscript-ai/gptscript v0.9.10-0.20260507143209-6761bb8f98e8/go.mod h1:uAs+TqMwCcTY+CftVfK89M4X7GYkjYZKGmL1v4sJ5wc= +github.com/gptscript-ai/gptscript v0.9.10-0.20260522151821-b374bab0897c h1:6FvlvAi5CcCTXfREkhAnpE8RuC9IJpP+rJjIIVk2hb4= +github.com/gptscript-ai/gptscript v0.9.10-0.20260522151821-b374bab0897c/go.mod h1:uAs+TqMwCcTY+CftVfK89M4X7GYkjYZKGmL1v4sJ5wc= github.com/gptscript-ai/tui v0.0.0-20250419050840-5e79e16786c9 h1:wQC8sKyeGA50WnCEG+Jo5FNRIkuX3HX8d3ubyWCCoI8= github.com/gptscript-ai/tui v0.0.0-20250419050840-5e79e16786c9/go.mod h1:iwHxuueg2paOak7zIg0ESBWx7A0wIHGopAratbgaPNY= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= diff --git a/pkg/api/authz/authz.go b/pkg/api/authz/authz.go index b71e3694a2..56c3048ba9 100644 --- a/pkg/api/authz/authz.go +++ b/pkg/api/authz/authz.go @@ -250,6 +250,14 @@ var ( // API Key authentication webhook (called by nanobot shim) // This endpoint validates the API key passed in the header "POST /api/api-keys/auth", + + // Since these are temporary and only the credstore token is allowed to access, + // then auth is handled in the handler. + "GET /api/credentials/tool.gpt", + "POST /api/credentials/store", + "POST /api/credentials/get", + "POST /api/credentials/list", + "POST /api/credentials/erase", }, types.GroupBasic: { diff --git a/pkg/api/handlers/credential.go b/pkg/api/handlers/credential.go new file mode 100644 index 0000000000..7517e92467 --- /dev/null +++ b/pkg/api/handlers/credential.go @@ -0,0 +1,205 @@ +package handlers + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "strings" + + "github.com/obot-platform/obot/apiclient/types" + "github.com/obot-platform/obot/pkg/api" + gclient "github.com/obot-platform/obot/pkg/gateway/client" + gtypes "github.com/obot-platform/obot/pkg/gateway/types" +) + +const tool = ` +Name: Obot Credential Store Helper +Share Tools: store, get, list, erase + +--- +name: store + +#!http://localhost:%d/api/credentials/store + +--- +name: get + +#!http://localhost:%[1]d/api/credentials/get + +--- +name: list + +#!http://localhost:%[1]d/api/credentials/list + +--- +name: erase + +#!http://localhost:%[1]d/api/credentials/erase +` + +type CredentialHandler struct { + tool string + token string +} + +func NewCredentialHandler(port int, token string) *CredentialHandler { + return &CredentialHandler{ + tool: fmt.Sprintf(tool, port), + token: token, + } +} + +func (h *CredentialHandler) Tool(req api.Context) error { + return req.Write(h.tool) +} + +func (h *CredentialHandler) Store(req api.Context) error { + if err := h.auth(req); err != nil { + return err + } + + var body struct { + ServerURL string + Username string + Secret string + } + + if err := req.Read(&body); err != nil { + return types.NewErrBadRequest("invalid request body: %v", err) + } + + if len(body.ServerURL) == 0 { + return types.NewErrBadRequest("serverURL is required") + } + + name, context, ok := strings.Cut(body.ServerURL, "///") + if !ok { + return types.NewErrBadRequest("invalid server URL: %s", body.ServerURL) + } + + var credEnv struct { + Env map[string]string + } + if err := json.Unmarshal([]byte(body.Secret), &credEnv); err != nil { + return types.NewErrBadRequest("invalid secret format: %v", err) + } + + cred := gtypes.Credential{ + Context: context, + Name: name, + Secrets: credEnv.Env, + } + + if err := req.GatewayClient.UpsertCredential(req.Context(), cred); err != nil { + return fmt.Errorf("failed to store credential: %w", err) + } + + req.WriteHeader(http.StatusOK) + + return nil +} + +func (h *CredentialHandler) Get(req api.Context) error { + if err := h.auth(req); err != nil { + return err + } + + serverURL, err := req.Body() + if err != nil || len(serverURL) == 0 { + return types.NewErrBadRequest("invalid request body: %v", err) + } + + name, context, ok := strings.Cut(string(serverURL), "///") + if !ok { + return types.NewErrBadRequest("invalid server URL: %s", string(serverURL)) + } + + cred, err := req.GatewayClient.RevealCredential(req.Context(), []string{context}, name) + if err != nil { + if errors.As(err, &gclient.CredentialNotFoundError{}) { + // Return empty credential when not found. + return req.Write(credentialResponse{}) + } + return err + } + + secret, _ := json.Marshal(map[string]any{"env": cred.Secrets}) + return req.Write(credentialResponse{ + ServerURL: string(serverURL), + Username: "tool", + Secret: string(secret), + }) +} + +func (h *CredentialHandler) List(req api.Context) error { + if err := h.auth(req); err != nil { + return err + } + + var contexts []string + if err := req.Read(&contexts); err != nil { + return types.NewErrBadRequest("invalid request body: %v", err) + } + + creds, err := req.GatewayClient.ListCredentials(req.Context(), gclient.ListCredentialsOptions{ + CredentialContexts: contexts, + AllContexts: len(contexts) == 0, + }) + if err != nil { + return fmt.Errorf("failed to list credentials: %w", err) + } + + var result []credentialResponse + for _, cred := range creds { + secret, _ := json.Marshal(map[string]any{"env": cred.Secrets}) + result = append(result, credentialResponse{ + ServerURL: fmt.Sprintf("%s///%s", cred.Name, cred.Context), + Username: "tool", + Secret: string(secret), + }) + } + + return req.Write(result) +} + +func (h *CredentialHandler) Erase(req api.Context) error { + if err := h.auth(req); err != nil { + return err + } + + serverURL, err := req.Body() + if err != nil || len(serverURL) == 0 { + return types.NewErrBadRequest("invalid request body: %v", err) + } + + name, context, ok := strings.Cut(string(serverURL), "///") + if !ok { + return types.NewErrBadRequest("invalid server URL: %s", string(serverURL)) + } + + if _, err := req.GatewayClient.DeleteCredential(req.Context(), context, name); err != nil { + return fmt.Errorf("failed to delete credential: %w", err) + } + + return nil +} + +func (h *CredentialHandler) auth(req api.Context) error { + if envHeader := req.Request.Header.Get("X-GPTScript-Env"); envHeader != "" { + for envVar := range strings.SplitSeq(envHeader, ",") { + token, ok := strings.CutPrefix(envVar, "OBOT_CREDSTORE_API_TOKEN=") + if ok && token == h.token { + return nil + } + } + } + + return types.NewErrHTTP(http.StatusUnauthorized, "missing or invalid token") +} + +type credentialResponse struct { + ServerURL string `json:"serverURL"` + Username string `json:"username"` + Secret string `json:"secret"` +} diff --git a/pkg/api/router/router.go b/pkg/api/router/router.go index 4fa2c254b5..56bf3d297d 100644 --- a/pkg/api/router/router.go +++ b/pkg/api/router/router.go @@ -65,6 +65,15 @@ func Router(ctx context.Context, services *services.Services) (http.Handler, err publishedArtifacts := handlers.NewPublishedArtifactHandler(services.ArtifactBlobStore, services.ArtifactBlobBucket) imagePullSecretsHandler := handlers.NewImagePullSecretHandler(services.MCPRuntimeBackend, services.MCPImagePullSecrets, services.MCPServerNamespace, services.ServiceNamespace, services.ServiceAccountName, services.LocalK8sClient, services.ServiceAccountIssuerURL, services.ServiceAccountIssuerError) + credHandler := handlers.NewCredentialHandler(services.HTTPListenPort, services.CredstoreToken) + + // Temporary credential handlers + mux.HandleFunc("GET /api/credentials/tool.gpt", credHandler.Tool) + mux.HandleFunc("POST /api/credentials/store", credHandler.Store) + mux.HandleFunc("POST /api/credentials/get", credHandler.Get) + mux.HandleFunc("POST /api/credentials/list", credHandler.List) + mux.HandleFunc("POST /api/credentials/erase", credHandler.Erase) + // Version mux.HandleFunc("GET /api/version", version.GetVersion) diff --git a/pkg/credstores/credstore.go b/pkg/credstores/credstore.go index f35826e3b7..bb2ab451c2 100644 --- a/pkg/credstores/credstore.go +++ b/pkg/credstores/credstore.go @@ -3,49 +3,12 @@ package credstores import ( "fmt" "strings" - - "github.com/gptscript-ai/gptscript/pkg/input" - "github.com/gptscript-ai/gptscript/pkg/loader" ) -func Init(toolRegistries []string, dsn, encryptionConfigFile string) (string, []string, error) { - // Set up database - switch { - case strings.HasPrefix(dsn, "sqlite://"): - return setUpSQLite(toolRegistries, dsn, encryptionConfigFile) - case strings.HasPrefix(dsn, "postgres://"): - return setUpPostgres(toolRegistries, dsn, encryptionConfigFile) - default: - return "", nil, fmt.Errorf("unsupported database for credentials %s", strings.Split(dsn, "://")[0]) - } -} - -func setUpPostgres(toolRegistries []string, dsn, encryptionConfigFile string) (string, []string, error) { - toolRef, err := resolveToolRef(toolRegistries, "credential-stores/postgres") - if err != nil { - return "", nil, err - } - - return toolRef, []string{ - "GPTSCRIPT_POSTGRES_DSN=" + dsn, - "GPTSCRIPT_ENCRYPTION_CONFIG_FILE=" + encryptionConfigFile, - }, nil -} - -func setUpSQLite(toolRegistries []string, dsn, encryptionConfigFile string) (string, []string, error) { - dbFile, err := GPTScriptSQLiteFile(dsn) - if err != nil { - return "", nil, err - } - - toolRef, err := resolveToolRef(toolRegistries, "credential-stores/sqlite") - if err != nil { - return "", nil, err - } - - return toolRef, []string{ - "GPTSCRIPT_SQLITE_FILE=" + dbFile, - "GPTSCRIPT_ENCRYPTION_CONFIG_FILE=" + encryptionConfigFile, +func Init(port int, token string) (string, []string, error) { + return fmt.Sprintf("http://localhost:%d/api/credentials/tool.gpt", port), []string{ + "GPTSCRIPT_HTTP_ENV=OBOT_CREDSTORE_API_TOKEN", + "OBOT_CREDSTORE_API_TOKEN=" + token, }, nil } @@ -62,24 +25,3 @@ func GPTScriptSQLiteFile(dsn string) (string, error) { return strings.TrimSuffix(dbFile, ".db") + "-credentials.db", nil } - -func resolveToolRef(toolRegistries []string, relToolPath string) (string, error) { - for _, toolRegistry := range toolRegistries { - if remapped := loader.Remap[toolRegistry]; remapped != "" { - toolRegistry = remapped - } - - // This doesn't support registry references with revisions; e.g. `@` - ref := fmt.Sprintf("%s/%s", toolRegistry, relToolPath) - content, err := input.FromLocation(ref+"/tool.gpt", true) - if err != nil || content == "" { - continue - } - - // Note: We could parse the content here to be extra sure the tool we're looking for exists, - // but this is probably good enough for now. - return ref, nil - } - - return "", fmt.Errorf("%q not found in provided tool registries", relToolPath) -} diff --git a/pkg/encryption/encryption.go b/pkg/encryption/encryption.go index ac5ae782ef..f3765176bd 100644 --- a/pkg/encryption/encryption.go +++ b/pkg/encryption/encryption.go @@ -58,35 +58,35 @@ func (o *Options) Validate() error { return nil } -func Init(ctx context.Context, opts Options) (*encryptionconfig.EncryptionConfiguration, string, error) { +func Init(ctx context.Context, opts Options) (*encryptionconfig.EncryptionConfiguration, error) { if err := opts.Validate(); err != nil { - return nil, "", err + return nil, err } // Set up encryption provider switch strings.ToLower(opts.EncryptionProvider) { case "aws": if err := setUpAWSKMS(ctx, opts.AWSKMSKeyARN, opts.EncryptionConfigFile); err != nil { - return nil, "", fmt.Errorf("failed to setup AWS KMS: %w", err) + return nil, fmt.Errorf("failed to setup AWS KMS: %w", err) } case "gcp": if err := setUpGoogleKMS(ctx, opts.GCPKMSKeyURI, opts.EncryptionConfigFile); err != nil { - return nil, "", fmt.Errorf("failed to setup Google Cloud KMS: %w", err) + return nil, fmt.Errorf("failed to setup Google Cloud KMS: %w", err) } case "azure": if err := setUpAzureKeyVault(ctx, opts.AzureKeyVaultName, opts.AzureKeyName, opts.AzureKeyVersion, opts.EncryptionConfigFile); err != nil { - return nil, "", fmt.Errorf("failed to setup Azure Key Vault: %w", err) + return nil, fmt.Errorf("failed to setup Azure Key Vault: %w", err) } } if opts.EncryptionConfigFile != "" { log.Infof("Encryption: Using encryption config file: %s", opts.EncryptionConfigFile) ec, err := encryptionconfig.LoadEncryptionConfig(ctx, opts.EncryptionConfigFile, false, "obot") - return ec, opts.EncryptionConfigFile, err + return ec, err } log.Warnf("Encryption: No encryption config file provided, using unencrypted storage") - return nil, "", nil + return nil, nil } func setUpAzureKeyVault(ctx context.Context, keyvaultName, keyName, keyVersion, configFile string) error { diff --git a/pkg/server/server.go b/pkg/server/server.go index fa92de66fb..65d449a94d 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "io" "net/http" "time" @@ -20,7 +21,7 @@ var log = logger.Package() func Run(ctx context.Context, c services.Config) error { servicesCtx, servicesCancel := context.WithCancel(context.Background()) defer servicesCancel() - svcs, err := services.New(servicesCtx, c) + svcs, start, err := services.New(servicesCtx, c) if err != nil { return err } @@ -107,6 +108,41 @@ func Run(ctx context.Context, c services.Config) error { svcs.GatewayClient.Close() }) + if start != nil { + go func() { + httpClient := &http.Client{ + Timeout: 5 * time.Second, + } + + for range 10 { + select { + case <-shutdown: + return + case <-ctx.Done(): + return + case <-time.After(200 * time.Millisecond): + } + + resp, err := httpClient.Get(fmt.Sprintf("http://localhost:%d/api/healthz", c.HTTPListenPort)) + if err != nil { + continue + } + + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + + if resp.StatusCode == http.StatusOK { + log.Infof("Server is ready to accept requests") + break + } + } + + // If the server isn't ready after 10 tries, we will call this anyway. + // However, there will be other failures so fine as a short-term hack. + start() + }() + } + if err = s.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { return err } diff --git a/pkg/services/config.go b/pkg/services/config.go index 259ad5b613..461e3fb4fa 100644 --- a/pkg/services/config.go +++ b/pkg/services/config.go @@ -3,6 +3,7 @@ package services import ( "bytes" "context" + "crypto/rand" "encoding/json" "errors" "fmt" @@ -155,6 +156,8 @@ type Services struct { InternalServerURL string DevUIPort int UserUIPort int + HTTPListenPort int + CredstoreToken string StorageClient storage.Client Router *router.Router GPTClient *gptscript.GPTScript @@ -419,20 +422,22 @@ func newGPTScript(ctx context.Context, credStore string, credStoreEnv []string, mcpSessionManager *mcp.SessionManager, -) (*gptscript.GPTScript, error) { +) (*gptscript.GPTScript, func(), error) { if os.Getenv("GPTSCRIPT_URL") != "" { - return gptscript.NewGPTScript(gptscript.GlobalOptions{ + g, err := gptscript.NewGPTScript(gptscript.GlobalOptions{ URL: os.Getenv("GPTSCRIPT_URL"), WorkspaceTool: workspaceTool, DatasetTool: datasetTool, }) + + return g, nil, err } credOverrides := strings.Split(os.Getenv("GPTSCRIPT_CREDENTIAL_OVERRIDE"), ",") if len(credOverrides) == 1 && strings.TrimSpace(credOverrides[0]) == "" { credOverrides = nil } - url, err := sdkserver.EmbeddedStart(ctx, sdkserver.Options{ + url, start, err := sdkserver.EmbeddedStart(ctx, sdkserver.Options{ Options: gptscriptai.Options{ Env: copyKeys(envPassThrough), Cache: cache.Options{ @@ -451,34 +456,36 @@ func newGPTScript(ctx context.Context, MCPLoader: mcpSessionManager, }) if err != nil { - return nil, err + return nil, nil, err } if mcpSessionManager != nil { if err := os.Setenv("GPTSCRIPT_URL", url); err != nil { - return nil, err + return nil, nil, err } } if os.Getenv("WORKSPACE_PROVIDER_DATA_HOME") == "" { if err = os.Setenv("WORKSPACE_PROVIDER_DATA_HOME", filepath.Join(xdg.DataHome, "obot", "workspace-provider")); err != nil { - return nil, err + return nil, nil, err } } - return gptscript.NewGPTScript(gptscript.GlobalOptions{ + g, err := gptscript.NewGPTScript(gptscript.GlobalOptions{ Env: copyKeys(envPassThrough), URL: url, WorkspaceTool: workspaceTool, DatasetTool: datasetTool, }) + + return g, start, err } -func New(ctx context.Context, config Config) (*Services, error) { +func New(ctx context.Context, config Config) (*Services, func(), error) { // Setup Otel first so other services can use it. otel, err := newOtel(ctx) if err != nil { - return nil, fmt.Errorf("failed to bootstrap OTel SDK: %w", err) + return nil, nil, fmt.Errorf("failed to bootstrap OTel SDK: %w", err) } system.SetBinToSelf() @@ -494,10 +501,10 @@ func New(ctx context.Context, config Config) (*Services, error) { } oauthClientExpiration, err := otime.ParseDuration(config.MCPOAuthClientExpiration) if err != nil { - return nil, fmt.Errorf("invalid MCP OAuth client expiration: %w", err) + return nil, nil, fmt.Errorf("invalid MCP OAuth client expiration: %w", err) } if oauthClientExpiration < time.Minute { - return nil, fmt.Errorf("invalid MCP OAuth client expiration: must be at least 1 minute") + return nil, nil, fmt.Errorf("invalid MCP OAuth client expiration: must be at least 1 minute") } runtimeIsK8s := mcp.IsKubernetesBackend(config.MCPRuntimeBackend) @@ -508,7 +515,7 @@ func New(ctx context.Context, config Config) (*Services, error) { // Validate network policy provider configuration mcpNetworkPolicyEnabled := config.MCPNetworkPolicyProviderChartPath != "" || config.MCPNetworkPolicyProviderChartName != "" if mcpNetworkPolicyEnabled && !runtimeIsK8s { - return nil, fmt.Errorf("network policy provider requires MCP runtime backend to be kubernetes") + return nil, nil, fmt.Errorf("network policy provider requires MCP runtime backend to be kubernetes") } if !mcpNetworkPolicyEnabled { config.MCPNetworkPolicyProviderChartRepo = "" @@ -521,10 +528,10 @@ func New(ctx context.Context, config Config) (*Services, error) { (config.MCPNetworkPolicyProviderChartRepo != "" || config.MCPNetworkPolicyProviderChartName != "" || config.MCPNetworkPolicyProviderChartVersion != "") { - return nil, fmt.Errorf("network policy provider chart path cannot be combined with chart repo, name, or version") + return nil, nil, fmt.Errorf("network policy provider chart path cannot be combined with chart repo, name, or version") } if config.MCPNetworkPolicyProviderChartPath == "" && config.MCPNetworkPolicyProviderChartRepo == "" { - return nil, fmt.Errorf("network policy provider requires chart repo when using a remote chart") + return nil, nil, fmt.Errorf("network policy provider requires chart repo when using a remote chart") } } @@ -534,7 +541,7 @@ func New(ctx context.Context, config Config) (*Services, error) { storageClient, restConfig, dbAccess, storageServices, err := storage.Start(ctx, config.Config) if err != nil { pkgLog.Errorf("Failed to connect to database: dsn=%s error=%v", sanitizedDSN, err) - return nil, err + return nil, nil, err } pkgLog.Infof("Successfully connected to database: dsn=%s", sanitizedDSN) @@ -550,25 +557,26 @@ func New(ctx context.Context, config Config) (*Services, error) { gatewayDB, err := db.New(dbAccess.DB, dbAccess.SQLDB, true) if err != nil { pkgLog.Errorf("Failed to initialize gateway database: error=%v", err) - return nil, err + return nil, nil, err } // Important: the database needs to be auto-migrated before we create the cred store, so that // the gptscript_credentials table is available. pkgLog.Infof("Running database migrations") if err := gatewayDB.AutoMigrate(); err != nil { pkgLog.Errorf("Failed to run database migrations: error=%v", err) - return nil, err + return nil, nil, err } pkgLog.Infof("Database migrations completed successfully") - encryptionConfig, encryptionConfigFile, err := encryption.Init(ctx, encryption.Options(config.EncryptionConfig)) + encryptionConfig, err := encryption.Init(ctx, encryption.Options(config.EncryptionConfig)) if err != nil { - return nil, err + return nil, nil, err } - credStore, credStoreEnv, err := credstores.Init(config.ToolRegistries, config.DSN, encryptionConfigFile) + credstoreToken := strings.ToLower(rand.Text() + rand.Text()) + credStore, credStoreEnv, err := credstores.Init(config.HTTPListenPort, credstoreToken) if err != nil { - return nil, err + return nil, nil, err } if config.DevMode { @@ -603,7 +611,7 @@ func New(ctx context.Context, config Config) (*Services, error) { config.MCPAuditLogRetentionDays, ) if err := migrateGPTScriptCredentials(ctx, gatewayClient, gatewayDB, config.DSN); err != nil { - return nil, fmt.Errorf("failed to migrate GPTScript credentials: %w", err) + return nil, nil, fmt.Errorf("failed to migrate GPTScript credentials: %w", err) } storageServices.Authn.SetServiceAccountValidator(func(ctx context.Context, token string) (string, error) { apiKey, err := gatewayClient.ValidateStorageServiceAccountToken(ctx, token) @@ -630,7 +638,7 @@ func New(ctx context.Context, config Config) (*Services, error) { if mcp.IsKubernetesBackend(config.MCPRuntimeBackend) { localK8sConfig, err = BuildLocalK8sConfig() if err != nil { - return nil, fmt.Errorf("failed to build local Kubernetes config: %w", err) + return nil, nil, fmt.Errorf("failed to build local Kubernetes config: %w", err) } serviceAccountIssuerURL, err = imagepullsecrets.DiscoverServiceAccountIssuer(ctx, localK8sConfig) if err != nil { @@ -643,13 +651,13 @@ func New(ctx context.Context, config Config) (*Services, error) { // PSA settings are always sourced from Helm/environment and cannot be modified via UI psaSettings, err := parsePSASettingsFromHelm(mcp.Options(config.MCPConfig)) if err != nil { - return nil, err + return nil, nil, err } // Pod scheduling settings (affinity, tolerations, resources, runtimeClassName) can be managed // via Helm OR UI. If set via Helm, SetViaHelm=true and UI cannot modify them. podSchedulingSettings, err := parsePodSchedulingSettingsFromHelm(mcp.Options(config.MCPConfig)) if err != nil { - return nil, err + return nil, nil, err } var postgresDSN string @@ -659,7 +667,7 @@ func New(ctx context.Context, config Config) (*Services, error) { persistentTokenServer, err := persistent.NewTokenService(config.Hostname, gatewayClient) if err != nil { - return nil, fmt.Errorf("failed to setup persistent token service: %w", err) + return nil, nil, fmt.Errorf("failed to setup persistent token service: %w", err) } r, err := nah.NewRouter("obot-controller", &nah.Options{ @@ -669,18 +677,18 @@ func New(ctx context.Context, config Config) (*Services, error) { HealthzPort: -1, }) if err != nil { - return nil, err + return nil, nil, err } // Set up MCPWebhookValidation indexer mcpWebhookValidationGVK, err := r.Backend().GroupVersionKindFor(&v1.MCPWebhookValidation{}) if err != nil { - return nil, err + return nil, nil, err } mcpWebhookValidationInformer, err := r.Backend().GetInformerForKind(ctx, mcpWebhookValidationGVK) if err != nil { - return nil, err + return nil, nil, err } if err = mcpWebhookValidationInformer.AddIndexers(map[string]gocache.IndexFunc{ @@ -725,37 +733,37 @@ func New(ctx context.Context, config Config) (*Services, error) { return results, nil }, }); err != nil { - return nil, err + return nil, nil, err } webhookHelper := mcp.NewWebhookHelper(mcpWebhookValidationInformer.GetIndexer(), config.Hostname) mcpSessionManager, err := mcp.NewSessionManager(ctx, config.EnableAuthentication, persistentTokenServer, config.Hostname, config.HTTPListenPort, mcp.Options(config.MCPConfig), webhookHelper, localK8sConfig, storageClient) if err != nil { - return nil, err + return nil, nil, err } var apiLocalK8sClient kclient.Client if localK8sConfig != nil { apiLocalK8sClient, err = kclient.New(localK8sConfig, kclient.Options{Scheme: k8sscheme.Scheme}) if err != nil { - return nil, fmt.Errorf("failed to build local k8s client for API server: %w", err) + return nil, nil, fmt.Errorf("failed to build local k8s client for API server: %w", err) } } - gptscriptClient, err := newGPTScript(ctx, config.EnvKeys, credStore, credStoreEnv, mcpSessionManager) + gptscriptClient, start, err := newGPTScript(ctx, config.EnvKeys, credStore, credStoreEnv, mcpSessionManager) if err != nil { - return nil, err + return nil, nil, err } acrGVK, err := r.Backend().GroupVersionKindFor(&v1.AccessControlRule{}) if err != nil { - return nil, err + return nil, nil, err } acrInformer, err := r.Backend().GetInformerForKind(ctx, acrGVK) if err != nil { - return nil, err + return nil, nil, err } if err = acrInformer.AddIndexers(map[string]gocache.IndexFunc{ @@ -800,19 +808,19 @@ func New(ctx context.Context, config Config) (*Services, error) { return results, nil }, }); err != nil { - return nil, err + return nil, nil, err } acrHelper := accesscontrolrule.NewAccessControlRuleHelper(acrInformer.GetIndexer(), r.Backend()) skillAccessRuleGVK, err := r.Backend().GroupVersionKindFor(&v1.SkillAccessRule{}) if err != nil { - return nil, err + return nil, nil, err } skillAccessRuleInformer, err := r.Backend().GetInformerForKind(ctx, skillAccessRuleGVK) if err != nil { - return nil, err + return nil, nil, err } if err = skillAccessRuleInformer.AddIndexers(map[string]gocache.IndexFunc{ @@ -877,14 +885,14 @@ func New(ctx context.Context, config Config) (*Services, error) { return results, nil }, }); err != nil { - return nil, err + return nil, nil, err } skillAccessRuleHelper := skillaccessrule.NewHelper(skillAccessRuleInformer.GetIndexer()) mapHelper, err := modelaccesspolicy.NewHelper(ctx, r.Backend()) if err != nil { - return nil, err + return nil, nil, err } invoker := invoke.NewInvoker( @@ -902,7 +910,7 @@ func New(ctx context.Context, config Config) (*Services, error) { if config.EnableMessagePolicies { msgPolicyHelper, err = messagepolicy.NewHelper(ctx, r.Backend(), storageClient, providerDispatcher, gatewayClient) if err != nil { - return nil, err + return nil, nil, err } } @@ -912,13 +920,13 @@ func New(ctx context.Context, config Config) (*Services, error) { var proxyManager *proxy.Manager bootstrapper, err := bootstrap.New(ctx, config.Hostname, gatewayClient, config.EnableAuthentication, config.ForceEnableBootstrap) if err != nil { - return nil, err + return nil, nil, err } gatewayOpts := gserver.Options(config.GatewayConfig) gatewayServer, err := gserver.New(ctx, gatewayDB, persistentTokenServer, providerDispatcher, acrHelper, mapHelper, msgPolicyHelper, gatewayOpts) if err != nil { - return nil, err + return nil, nil, err } authenticators := gserver.NewGatewayTokenReviewer(gatewayClient, providerDispatcher) @@ -951,7 +959,7 @@ func New(ctx context.Context, config Config) (*Services, error) { ProviderUserID: "nobody", HashedProviderUserID: hash.String("nobody"), }); err != nil { - return nil, fmt.Errorf(`failed to remove "nobody" user and identity from database: %w`, err) + return nil, nil, fmt.Errorf(`failed to remove "nobody" user and identity from database: %w`, err) } else if id != 0 { // Create this UserDelete object so that their stuff gets deleted. if err = storageClient.Create(ctx, &v1.UserDelete{ @@ -963,7 +971,7 @@ func New(ctx context.Context, config Config) (*Services, error) { UserID: id, }, }); err != nil { - return nil, fmt.Errorf(`failed to create "nobody" user delete object: %w`, err) + return nil, nil, fmt.Errorf(`failed to create "nobody" user delete object: %w`, err) } } } else { @@ -981,12 +989,12 @@ func New(ctx context.Context, config Config) (*Services, error) { auditLogger, err := audit.New(ctx, audit.Options(config.AuditConfig)) if err != nil { - return nil, fmt.Errorf("failed to create audit logger: %w", err) + return nil, nil, fmt.Errorf("failed to create audit logger: %w", err) } rateLimiter, err := ratelimiter.New(ratelimiter.Options(config.RateLimiterConfig)) if err != nil { - return nil, fmt.Errorf("failed to create rate limiter: %w", err) + return nil, nil, fmt.Errorf("failed to create rate limiter: %w", err) } // Derive registryNoAuth flag from config @@ -1014,6 +1022,8 @@ func New(ctx context.Context, config Config) (*Services, error) { InternalServerURL: fmt.Sprintf("http://localhost:%d", config.HTTPListenPort), DevUIPort: devPort, UserUIPort: config.UserUIPort, + HTTPListenPort: config.HTTPListenPort, + CredstoreToken: credstoreToken, ToolRegistryURLs: config.ToolRegistries, StorageClient: storageClient, Router: r, @@ -1094,17 +1104,17 @@ func New(ctx context.Context, config Config) (*Services, error) { } if (config.ArtifactStorageProvider == "") != (config.ArtifactStorageBucket == "") { - return nil, fmt.Errorf("both OBOT_ARTIFACT_STORAGE_PROVIDER and OBOT_ARTIFACT_STORAGE_BUCKET must be set together") + return nil, nil, fmt.Errorf("both OBOT_ARTIFACT_STORAGE_PROVIDER and OBOT_ARTIFACT_STORAGE_BUCKET must be set together") } if config.ArtifactStorageProvider != "" && config.ArtifactStorageBucket != "" { artifactStorageConfig := buildArtifactStorageConfig(config) artifactBlobStore, err := blob.New(apiclienttypes.StorageProviderType(config.ArtifactStorageProvider), artifactStorageConfig) if err != nil { - return nil, fmt.Errorf("failed to create artifact blob store: %w", err) + return nil, nil, fmt.Errorf("failed to create artifact blob store: %w", err) } if err := artifactBlobStore.Test(ctx); err != nil { - return nil, fmt.Errorf("failed to validate artifact blob store: %w", err) + return nil, nil, fmt.Errorf("failed to validate artifact blob store: %w", err) } svcs.ArtifactBlobStore = artifactBlobStore } else { @@ -1112,13 +1122,13 @@ func New(ctx context.Context, config Config) (*Services, error) { defaultDir := filepath.Join(xdg.DataHome, "obot", "published-artifacts") artifactBlobStore, err := blob.NewDirectoryStore(defaultDir) if err != nil { - return nil, fmt.Errorf("failed to create local artifact blob store: %w", err) + return nil, nil, fmt.Errorf("failed to create local artifact blob store: %w", err) } svcs.ArtifactBlobStore = artifactBlobStore svcs.ArtifactBlobBucket = "default" } - return svcs, nil + return svcs, start, nil } func migrateGPTScriptCredentials(ctx context.Context, gatewayClient *client.Client, gatewayDB *db.DB, dsn string) error {