Skip to content

Commit 8761cb8

Browse files
feat: add support for OAuth PATs (#1387)
feat: add support for OAuth prompt tokens Instead of configuring and using OAuth, an app can specify that it supports using tokens. If this is the case, then the tool's credential should list some number of "prompt_tokens" and, optionally, "prompt_vars" and the user will be prompted for those. If the oauth2 credential tool should prompt the user for a token instead of using OAuth, then Obot will not pass the environment variables that feed the URLs to the tool. A side effect of this change is that OAuth apps no longer default to global. Signed-off-by: Donnie Adams <[email protected]> Co-authored-by: Ivy <[email protected]>
1 parent b350b55 commit 8761cb8

File tree

31 files changed

+575
-361
lines changed

31 files changed

+575
-361
lines changed

apiclient/types/oauthapp.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ type OAuthAppManifest struct {
3636
// This field is optional for HubSpot OAuth apps.
3737
OptionalScope string `json:"optionalScope,omitempty"`
3838
// This field is required, it correlates to the integration name in the gptscript oauth cred tool
39-
Integration string `json:"integration,omitempty"`
39+
Alias string `json:"alias,omitempty"`
4040
// Global indicates if the OAuth app is globally applied to all agents.
41-
Global *bool `json:"global,omitempty"`
41+
Global bool `json:"global,omitempty"`
4242
// This field is only used by Salesforce
4343
InstanceURL string `json:"instanceURL,omitempty"`
4444
}

apiclient/types/zz_generated.deepcopy.go

Lines changed: 0 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/api/handlers/agent.go

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -734,12 +734,15 @@ func (a *AgentHandler) EnsureCredentialForKnowledgeSource(req api.Context) error
734734
return req.WriteCreated(resp)
735735
}
736736

737-
credentialTools, err := v1.CredentialTools(req.Context(), req.Storage, req.Namespace(), ref)
738-
if err != nil {
739-
return err
737+
var toolReference v1.ToolReference
738+
if err := req.Get(&toolReference, ref); err != nil {
739+
return fmt.Errorf("failed to get tool reference %v", ref)
740+
}
741+
if toolReference.Status.Tool == nil {
742+
return types.NewErrHttp(http.StatusTooEarly, "tool reference is not ready yet")
740743
}
741744

742-
if len(credentialTools) == 0 {
745+
if len(toolReference.Status.Tool.Credentials) == 0 {
743746
// The only way to get here is if the controller hasn't set the field yet.
744747
if agent.Status.AuthStatus == nil {
745748
agent.Status.AuthStatus = make(map[string]types.OAuthAppLoginAuthStatus)
@@ -754,6 +757,10 @@ func (a *AgentHandler) EnsureCredentialForKnowledgeSource(req api.Context) error
754757
return req.WriteCreated(resp)
755758
}
756759

760+
if _, ok := toolReference.Status.Tool.Metadata["oauth"]; !ok {
761+
return types.NewErrBadRequest("tool reference %q does not have oauth metadata", ref)
762+
}
763+
757764
oauthLogin := &v1.OAuthAppLogin{
758765
ObjectMeta: metav1.ObjectMeta{
759766
Name: system.OAuthAppLoginPrefix + agent.Name + ref,
@@ -762,15 +769,15 @@ func (a *AgentHandler) EnsureCredentialForKnowledgeSource(req api.Context) error
762769
Spec: v1.OAuthAppLoginSpec{
763770
CredentialContext: agent.Name,
764771
ToolReference: ref,
765-
OAuthApps: agent.Spec.Manifest.OAuthApps,
772+
OAuthApps: []string{toolReference.Status.Tool.Metadata["oauth"]},
766773
},
767774
}
768775

769-
if err = req.Delete(oauthLogin); err != nil {
776+
if err := req.Delete(oauthLogin); err != nil {
770777
return err
771778
}
772779

773-
oauthLogin, err = wait.For(req.Context(), req.Storage, oauthLogin, func(obj *v1.OAuthAppLogin) (bool, error) {
780+
oauthLogin, err := wait.For(req.Context(), req.Storage, oauthLogin, func(obj *v1.OAuthAppLogin) (bool, error) {
774781
return obj.Status.External.Authenticated || obj.Status.External.Error != "" || obj.Status.External.URL != "", nil
775782
}, wait.Option{
776783
Create: true,
@@ -918,7 +925,7 @@ func runAuthForAgent(ctx context.Context, c kclient.WithWatch, invoker *invoke.I
918925

919926
var toolRef v1.ToolReference
920927
for _, tool := range tools {
921-
if strings.ContainsAny(tool, "./") {
928+
if render.IsExternalTool(tool) {
922929
prg, err := gClient.LoadFile(ctx, tool)
923930
if err != nil {
924931
return nil, err
@@ -965,7 +972,7 @@ func removeToolCredentials(ctx context.Context, client kclient.Client, gClient *
965972
credentialNames []string
966973
)
967974
for _, tool := range tools {
968-
if strings.ContainsAny(tool, "./") {
975+
if render.IsExternalTool(tool) {
969976
prg, err := gClient.LoadFile(ctx, tool)
970977
if err != nil {
971978
errs = append(errs, err)

pkg/api/handlers/workflows.go

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"net/http"
78
"strings"
89

910
"github.com/gptscript-ai/go-gptscript"
@@ -368,12 +369,15 @@ func (a *WorkflowHandler) EnsureCredentialForKnowledgeSource(req api.Context) er
368369
return req.WriteCreated(resp)
369370
}
370371

371-
credentialTools, err := v1.CredentialTools(req.Context(), req.Storage, req.Namespace(), ref)
372-
if err != nil {
373-
return err
372+
var toolReference v1.ToolReference
373+
if err := req.Get(&toolReference, ref); err != nil {
374+
return fmt.Errorf("failed to get tool reference %v", ref)
375+
}
376+
if toolReference.Status.Tool == nil {
377+
return types.NewErrHttp(http.StatusTooEarly, "tool reference is not ready yet")
374378
}
375379

376-
if len(credentialTools) == 0 {
380+
if len(toolReference.Status.Tool.Credentials) == 0 {
377381
// The only way to get here is if the controller hasn't set the field yet.
378382
if wf.Status.AuthStatus == nil {
379383
wf.Status.AuthStatus = make(map[string]types.OAuthAppLoginAuthStatus)
@@ -397,15 +401,15 @@ func (a *WorkflowHandler) EnsureCredentialForKnowledgeSource(req api.Context) er
397401
Spec: v1.OAuthAppLoginSpec{
398402
CredentialContext: wf.Name,
399403
ToolReference: ref,
400-
OAuthApps: wf.Spec.Manifest.OAuthApps,
404+
OAuthApps: []string{toolReference.Status.Tool.Metadata["oauth"]},
401405
},
402406
}
403407

404-
if err = req.Delete(oauthLogin); err != nil {
408+
if err := req.Delete(oauthLogin); err != nil {
405409
return err
406410
}
407411

408-
oauthLogin, err = wait.For(req.Context(), req.Storage, oauthLogin, func(obj *v1.OAuthAppLogin) (bool, error) {
412+
oauthLogin, err := wait.For(req.Context(), req.Storage, oauthLogin, func(obj *v1.OAuthAppLogin) (bool, error) {
409413
return obj.Status.External.Authenticated || obj.Status.External.Error != "" || obj.Status.External.URL != "", nil
410414
}, wait.Option{
411415
Create: true,

pkg/controller/handlers/toolinfo/toolinfo.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ package toolinfo
33
import (
44
"context"
55
"fmt"
6-
"strings"
76

87
"github.com/gptscript-ai/go-gptscript"
98
"github.com/obot-platform/nah/pkg/router"
109
"github.com/obot-platform/obot/apiclient/types"
1110
"github.com/obot-platform/obot/pkg/controller/creds"
11+
"github.com/obot-platform/obot/pkg/render"
1212
v1 "github.com/obot-platform/obot/pkg/storage/apis/obot.obot.ai/v1"
1313
apierror "k8s.io/apimachinery/pkg/api/errors"
1414
"k8s.io/apimachinery/pkg/util/sets"
@@ -57,7 +57,7 @@ func (h *Handler) SetToolInfoStatus(req router.Request, resp router.Response) (e
5757
credNames []string
5858
)
5959
for _, tool := range tools {
60-
if strings.ContainsAny(tool, "/.") {
60+
if render.IsExternalTool(tool) {
6161
credNames, err = h.credentialNamesForNonToolReferences(req.Ctx, tool)
6262
if err != nil {
6363
return err

pkg/gateway/server/oauth_apps.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,15 +114,15 @@ func (s *Server) createOAuthApp(apiContext api.Context) error {
114114
var existingApps v1.OAuthAppList
115115
if err := apiContext.Storage.List(apiContext.Context(), &existingApps, &kclient.ListOptions{
116116
FieldSelector: fields.SelectorFromSet(selectors.RemoveEmpty(map[string]string{
117-
"spec.manifest.integration": appManifest.Integration,
117+
"spec.manifest.alias": appManifest.Alias,
118118
})),
119119
Namespace: apiContext.Namespace(),
120120
}); err != nil {
121121
return err
122122
}
123123

124124
if len(existingApps.Items) > 0 {
125-
return types2.NewErrHttp(http.StatusConflict, fmt.Sprintf("OAuth app with integration %s already exists", appManifest.Integration))
125+
return types2.NewErrHttp(http.StatusConflict, fmt.Sprintf("OAuth app with alias %s already exists", appManifest.Alias))
126126
}
127127

128128
app := v1.OAuthApp{

pkg/gateway/types/oauth_apps.go

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,19 +47,23 @@ type OAuthAppTypeConfig struct {
4747

4848
func ValidateAndSetDefaultsOAuthAppManifest(r *types.OAuthAppManifest, create bool) error {
4949
var errs []error
50-
if r.Integration == "" {
51-
errs = append(errs, fmt.Errorf("missing integration"))
52-
} else if !alphaNumericRegexp.MatchString(r.Integration) {
53-
errs = append(errs, fmt.Errorf("integration name can only contain alphanumeric characters and hyphens: %s", r.Integration))
50+
if r.Alias == "" {
51+
errs = append(errs, fmt.Errorf("missing alias"))
52+
} else if !alphaNumericRegexp.MatchString(r.Alias) {
53+
errs = append(errs, fmt.Errorf("alias name can only contain alphanumeric characters and hyphens: %s", r.Alias))
5454
}
5555

5656
switch r.Type {
5757
case types.OAuthAppTypeAtlassian:
5858
r.AuthURL = AtlassianAuthorizeURL
5959
r.TokenURL = AtlassianTokenURL
6060
case types.OAuthAppTypeMicrosoft365:
61-
r.AuthURL = fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/v2.0/authorize", r.TenantID)
62-
r.TokenURL = fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/v2.0/token", r.TenantID)
61+
tenantID := r.TenantID
62+
if tenantID == "" {
63+
tenantID = "common"
64+
}
65+
r.AuthURL = fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/v2.0/authorize", tenantID)
66+
r.TokenURL = fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/v2.0/token", tenantID)
6367
case types.OAuthAppTypeSlack:
6468
r.AuthURL = SlackAuthorizeURL
6569
r.TokenURL = SlackTokenURL
@@ -163,16 +167,16 @@ func MergeOAuthAppManifests(r, other types.OAuthAppManifest) types.OAuthAppManif
163167
if other.Name != "" {
164168
retVal.Name = other.Name
165169
}
166-
if other.Integration != "" {
167-
retVal.Integration = other.Integration
170+
if other.Alias != "" {
171+
retVal.Alias = other.Alias
168172
}
169173
if other.AppID != "" {
170174
retVal.AppID = other.AppID
171175
}
172176
if other.OptionalScope != "" {
173177
retVal.OptionalScope = other.OptionalScope
174178
}
175-
if other.Global != nil {
179+
if other.Global {
176180
retVal.Global = other.Global
177181
}
178182

pkg/render/render.go

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,11 @@ func Agent(ctx context.Context, db kclient.Client, agent *v1.Agent, oauthServerU
7474
}
7575

7676
if opts.Thread != nil {
77-
for _, tool := range opts.Thread.Spec.Manifest.Tools {
78-
if !added && tool == knowledgeToolName {
77+
for _, t := range opts.Thread.Spec.Manifest.Tools {
78+
if !added && t == knowledgeToolName {
7979
continue
8080
}
81-
name, tools, err := Tool(ctx, db, agent.Namespace, tool)
81+
name, tools, err := tool(ctx, db, agent.Namespace, t)
8282
if err != nil {
8383
return nil, nil, err
8484
}
@@ -107,17 +107,18 @@ func Agent(ctx context.Context, db kclient.Client, agent *v1.Agent, oauthServerU
107107
}
108108
}
109109

110-
for _, tool := range agent.Spec.Manifest.Tools {
111-
if !added && tool == knowledgeToolName {
110+
for _, t := range agent.Spec.Manifest.Tools {
111+
if !added && t == knowledgeToolName {
112112
continue
113113
}
114-
name, tools, err := Tool(ctx, db, agent.Namespace, tool)
114+
name, tools, err := tool(ctx, db, agent.Namespace, t)
115115
if err != nil {
116116
return nil, nil, err
117117
}
118118
if name != "" {
119119
mainTool.Tools = append(mainTool.Tools, name)
120120
}
121+
121122
otherTools = append(otherTools, tools...)
122123
}
123124

@@ -161,30 +162,30 @@ func OAuthAppEnv(ctx context.Context, db kclient.Client, oauthAppNames []string,
161162
activeIntegrations := map[string]v1.OAuthApp{}
162163
for _, name := range slices.Sorted(maps.Keys(apps)) {
163164
app := apps[name]
164-
if app.Spec.Manifest.Global == nil || !*app.Spec.Manifest.Global || app.Spec.Manifest.ClientID == "" || app.Spec.Manifest.ClientSecret == "" || app.Spec.Manifest.Integration == "" {
165+
if !app.Spec.Manifest.Global || app.Spec.Manifest.ClientID == "" || app.Spec.Manifest.ClientSecret == "" || app.Spec.Manifest.Alias == "" {
165166
continue
166167
}
167-
activeIntegrations[app.Spec.Manifest.Integration] = app
168+
activeIntegrations[app.Spec.Manifest.Alias] = app
168169
}
169170

170171
for _, appRef := range oauthAppNames {
171172
app, ok := apps[appRef]
172173
if !ok {
173174
return nil, fmt.Errorf("oauth app %s not found", appRef)
174175
}
175-
if app.Spec.Manifest.Integration == "" {
176+
if app.Spec.Manifest.Alias == "" {
176177
return nil, fmt.Errorf("oauth app %s has no integration name", app.Name)
177178
}
178179
if app.Spec.Manifest.ClientID == "" || app.Spec.Manifest.ClientSecret == "" {
179180
return nil, fmt.Errorf("oauth app %s has no client id or secret", app.Name)
180181
}
181182

182-
activeIntegrations[app.Spec.Manifest.Integration] = app
183+
activeIntegrations[app.Spec.Manifest.Alias] = app
183184
}
184185

185186
for _, integration := range slices.Sorted(maps.Keys(activeIntegrations)) {
186187
app := activeIntegrations[integration]
187-
integrationEnv := strings.ReplaceAll(strings.ToUpper(app.Spec.Manifest.Integration), "-", "_")
188+
integrationEnv := strings.ReplaceAll(strings.ToUpper(app.Spec.Manifest.Alias), "-", "_")
188189

189190
extraEnv = append(extraEnv,
190191
fmt.Sprintf("GPTSCRIPT_OAUTH_%s_AUTH_URL=%s", integrationEnv, app.AuthorizeURL(serverURL)),
@@ -351,8 +352,8 @@ func oauthAppsByName(ctx context.Context, c kclient.Client, namespace string) (m
351352
}
352353

353354
for _, app := range apps.Items {
354-
if app.Spec.Manifest.Integration != "" {
355-
result[app.Spec.Manifest.Integration] = app
355+
if app.Spec.Manifest.Alias != "" {
356+
result[app.Spec.Manifest.Alias] = app
356357
}
357358
}
358359

pkg/render/tool.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,9 @@ END INSTRUCTIONS: TOOL %q`, tool.Spec.Manifest.Name, tool.Spec.Manifest.Context,
9393
return toolDefs, nil
9494
}
9595

96-
func Tool(ctx context.Context, c client.Client, ns, name string) (_ string, toolDefs []gptscript.ToolDef, _ error) {
96+
func tool(ctx context.Context, c client.Client, ns, name string) (string, []gptscript.ToolDef, error) {
9797
if !system.IsToolID(name) {
98-
name, err := ResolveToolReference(ctx, c, types.ToolReferenceTypeTool, ns, name)
98+
name, err := resolveToolReferenceWithMetadata(ctx, c, types.ToolReferenceTypeTool, ns, name)
9999
return name, nil, err
100100
}
101101

pkg/render/workflow.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ func IsExternalTool(tool string) bool {
2929
}
3030

3131
func ResolveToolReference(ctx context.Context, c kclient.Client, toolRefType types.ToolReferenceType, ns, name string) (string, error) {
32+
name, err := resolveToolReferenceWithMetadata(ctx, c, toolRefType, ns, name)
33+
return name, err
34+
}
35+
36+
func resolveToolReferenceWithMetadata(ctx context.Context, c kclient.Client, toolRefType types.ToolReferenceType, ns, name string) (string, error) {
3237
if IsExternalTool(name) {
3338
return name, nil
3439
}
@@ -39,6 +44,7 @@ func ResolveToolReference(ctx context.Context, c kclient.Client, toolRefType typ
3944
} else if err != nil {
4045
return "", err
4146
}
47+
4248
if toolRefType != "" && tool.Spec.Type != toolRefType {
4349
return name, fmt.Errorf("tool reference %s is not of type %s", name, toolRefType)
4450
}

pkg/storage/apis/obot.obot.ai/v1/oauthapp.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ type OAuthApp struct {
2626
}
2727

2828
func (r *OAuthApp) GetAliasName() string {
29-
return r.Spec.Manifest.Integration
29+
return r.Spec.Manifest.Alias
3030
}
3131

3232
func (r *OAuthApp) SetAssigned(bool) {}
@@ -48,32 +48,32 @@ func (r *OAuthApp) Has(field string) bool {
4848
func (r *OAuthApp) Get(field string) string {
4949
if r != nil {
5050
switch field {
51-
case "spec.manifest.integration":
52-
return r.Spec.Manifest.Integration
51+
case "spec.manifest.alias":
52+
return r.Spec.Manifest.Alias
5353
}
5454
}
5555

5656
return ""
5757
}
5858

5959
func (r *OAuthApp) FieldNames() []string {
60-
return []string{"spec.manifest.integration"}
60+
return []string{"spec.manifest.alias"}
6161
}
6262

6363
func (r *OAuthApp) RedirectURL(baseURL string) string {
64-
return fmt.Sprintf("%s/api/app-oauth/callback/%s", baseURL, r.Spec.Manifest.Integration)
64+
return fmt.Sprintf("%s/api/app-oauth/callback/%s", baseURL, r.Spec.Manifest.Alias)
6565
}
6666

6767
func OAuthAppGetTokenURL(baseURL string) string {
6868
return fmt.Sprintf("%s/api/app-oauth/get-token", baseURL)
6969
}
7070

7171
func (r *OAuthApp) AuthorizeURL(baseURL string) string {
72-
return fmt.Sprintf("%s/api/app-oauth/authorize/%s", baseURL, r.Spec.Manifest.Integration)
72+
return fmt.Sprintf("%s/api/app-oauth/authorize/%s", baseURL, r.Spec.Manifest.Alias)
7373
}
7474

7575
func (r *OAuthApp) RefreshURL(baseURL string) string {
76-
return fmt.Sprintf("%s/api/app-oauth/refresh/%s", baseURL, r.Spec.Manifest.Integration)
76+
return fmt.Sprintf("%s/api/app-oauth/refresh/%s", baseURL, r.Spec.Manifest.Alias)
7777
}
7878

7979
func (r *OAuthApp) DeleteRefs() []Ref {

0 commit comments

Comments
 (0)