Skip to content

Commit 90e7868

Browse files
authored
feat: sdkserver: add credential routes (#846)
Signed-off-by: Grant Linville <[email protected]>
1 parent c0f116c commit 90e7868

File tree

6 files changed

+197
-4
lines changed

6 files changed

+197
-4
lines changed

pkg/cli/credential.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ func (c *Credential) Run(cmd *cobra.Command, _ []string) error {
4545

4646
ctx := c.root.CredentialContext
4747
if c.AllContexts {
48-
ctx = "*"
48+
ctx = credentials.AllCredentialContexts
4949
}
5050

5151
opts, err := c.root.NewGPTScriptOpts()

pkg/credentials/store.go

+7-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ import (
1111
"github.com/gptscript-ai/gptscript/pkg/config"
1212
)
1313

14+
const (
15+
DefaultCredentialContext = "default"
16+
AllCredentialContexts = "*"
17+
)
18+
1419
type CredentialBuilder interface {
1520
EnsureCredentialHelpers(ctx context.Context) error
1621
}
@@ -105,7 +110,7 @@ func (s Store) List(ctx context.Context) ([]Credential, error) {
105110
if err != nil {
106111
return nil, err
107112
}
108-
if s.credCtx == "*" || c.Context == s.credCtx {
113+
if s.credCtx == AllCredentialContexts || c.Context == s.credCtx {
109114
creds = append(creds, c)
110115
}
111116
}
@@ -139,7 +144,7 @@ func validateCredentialCtx(ctx string) error {
139144
return fmt.Errorf("credential context cannot be empty")
140145
}
141146

142-
if ctx == "*" { // this represents "all contexts" and is allowed
147+
if ctx == AllCredentialContexts {
143148
return nil
144149
}
145150

pkg/gptscript/gptscript.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ func Complete(opts ...Options) Options {
7575
result.Env = os.Environ()
7676
}
7777
if result.CredentialContext == "" {
78-
result.CredentialContext = "default"
78+
result.CredentialContext = credentials.DefaultCredentialContext
7979
}
8080

8181
return result

pkg/sdkserver/credentials.go

+176
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
package sdkserver
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"net/http"
7+
8+
"github.com/gptscript-ai/gptscript/pkg/config"
9+
gcontext "github.com/gptscript-ai/gptscript/pkg/context"
10+
"github.com/gptscript-ai/gptscript/pkg/credentials"
11+
"github.com/gptscript-ai/gptscript/pkg/repos/runtimes"
12+
)
13+
14+
func (s *server) initializeCredentialStore(ctx string) (credentials.CredentialStore, error) {
15+
cfg, err := config.ReadCLIConfig(s.gptscriptOpts.OpenAI.ConfigFile)
16+
if err != nil {
17+
return nil, fmt.Errorf("failed to read CLI config: %w", err)
18+
}
19+
20+
// TODO - are we sure we want to always use runtimes.Default here?
21+
store, err := credentials.NewStore(cfg, runtimes.Default(s.gptscriptOpts.Cache.CacheDir), ctx, s.gptscriptOpts.Cache.CacheDir)
22+
if err != nil {
23+
return nil, fmt.Errorf("failed to initialize credential store: %w", err)
24+
}
25+
26+
return store, nil
27+
}
28+
29+
func (s *server) listCredentials(w http.ResponseWriter, r *http.Request) {
30+
logger := gcontext.GetLogger(r.Context())
31+
req := new(credentialsRequest)
32+
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
33+
writeError(logger, w, http.StatusBadRequest, fmt.Errorf("invalid request body: %w", err))
34+
return
35+
}
36+
37+
if req.AllContexts {
38+
req.Context = credentials.AllCredentialContexts
39+
} else if req.Context == "" {
40+
req.Context = credentials.DefaultCredentialContext
41+
}
42+
43+
store, err := s.initializeCredentialStore(req.Context)
44+
if err != nil {
45+
writeError(logger, w, http.StatusInternalServerError, err)
46+
return
47+
}
48+
49+
creds, err := store.List(r.Context())
50+
if err != nil {
51+
writeError(logger, w, http.StatusInternalServerError, fmt.Errorf("failed to list credentials: %w", err))
52+
return
53+
}
54+
55+
// Remove the environment variable values (which are secrets) and refresh tokens from the response.
56+
for i := range creds {
57+
for k := range creds[i].Env {
58+
creds[i].Env[k] = ""
59+
}
60+
creds[i].RefreshToken = ""
61+
}
62+
63+
writeResponse(logger, w, map[string]any{"stdout": creds})
64+
}
65+
66+
func (s *server) createCredential(w http.ResponseWriter, r *http.Request) {
67+
logger := gcontext.GetLogger(r.Context())
68+
req := new(credentialsRequest)
69+
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
70+
writeError(logger, w, http.StatusBadRequest, fmt.Errorf("invalid request body: %w", err))
71+
return
72+
}
73+
74+
cred := new(credentials.Credential)
75+
if err := json.Unmarshal([]byte(req.Content), cred); err != nil {
76+
writeError(logger, w, http.StatusBadRequest, fmt.Errorf("invalid credential: %w", err))
77+
return
78+
}
79+
80+
if cred.Context == "" {
81+
cred.Context = credentials.DefaultCredentialContext
82+
}
83+
84+
store, err := s.initializeCredentialStore(cred.Context)
85+
if err != nil {
86+
writeError(logger, w, http.StatusInternalServerError, err)
87+
return
88+
}
89+
90+
if err := store.Add(r.Context(), *cred); err != nil {
91+
writeError(logger, w, http.StatusInternalServerError, fmt.Errorf("failed to create credential: %w", err))
92+
return
93+
}
94+
95+
writeResponse(logger, w, map[string]any{"stdout": "Credential created successfully"})
96+
}
97+
98+
func (s *server) revealCredential(w http.ResponseWriter, r *http.Request) {
99+
logger := gcontext.GetLogger(r.Context())
100+
req := new(credentialsRequest)
101+
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
102+
writeError(logger, w, http.StatusBadRequest, fmt.Errorf("invalid request body: %w", err))
103+
return
104+
}
105+
106+
if req.Name == "" {
107+
writeError(logger, w, http.StatusBadRequest, fmt.Errorf("missing credential name"))
108+
return
109+
}
110+
111+
if req.AllContexts || req.Context == credentials.AllCredentialContexts {
112+
writeError(logger, w, http.StatusBadRequest, fmt.Errorf("allContexts is not supported for credential retrieval; please specify the specific context that the credential is in"))
113+
return
114+
} else if req.Context == "" {
115+
req.Context = credentials.DefaultCredentialContext
116+
}
117+
118+
store, err := s.initializeCredentialStore(req.Context)
119+
if err != nil {
120+
writeError(logger, w, http.StatusInternalServerError, err)
121+
return
122+
}
123+
124+
cred, ok, err := store.Get(r.Context(), req.Name)
125+
if err != nil {
126+
writeError(logger, w, http.StatusInternalServerError, fmt.Errorf("failed to get credential: %w", err))
127+
return
128+
} else if !ok {
129+
writeError(logger, w, http.StatusNotFound, fmt.Errorf("credential not found"))
130+
return
131+
}
132+
133+
writeResponse(logger, w, map[string]any{"stdout": cred})
134+
}
135+
136+
func (s *server) deleteCredential(w http.ResponseWriter, r *http.Request) {
137+
logger := gcontext.GetLogger(r.Context())
138+
req := new(credentialsRequest)
139+
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
140+
writeError(logger, w, http.StatusBadRequest, fmt.Errorf("invalid request body: %w", err))
141+
}
142+
143+
if req.Name == "" {
144+
writeError(logger, w, http.StatusBadRequest, fmt.Errorf("missing credential name"))
145+
return
146+
}
147+
148+
if req.AllContexts || req.Context == credentials.AllCredentialContexts {
149+
writeError(logger, w, http.StatusBadRequest, fmt.Errorf("allContexts is not supported for credential deletion; please specify the specific context that the credential is in"))
150+
return
151+
} else if req.Context == "" {
152+
req.Context = credentials.DefaultCredentialContext
153+
}
154+
155+
store, err := s.initializeCredentialStore(req.Context)
156+
if err != nil {
157+
writeError(logger, w, http.StatusInternalServerError, err)
158+
return
159+
}
160+
161+
// Check to see if a cred exists so we can return a 404 if it doesn't.
162+
if _, ok, err := store.Get(r.Context(), req.Name); err != nil {
163+
writeError(logger, w, http.StatusInternalServerError, fmt.Errorf("failed to get credential: %w", err))
164+
return
165+
} else if !ok {
166+
writeError(logger, w, http.StatusNotFound, fmt.Errorf("credential not found"))
167+
return
168+
}
169+
170+
if err := store.Remove(r.Context(), req.Name); err != nil {
171+
writeError(logger, w, http.StatusInternalServerError, fmt.Errorf("failed to delete credential: %w", err))
172+
return
173+
}
174+
175+
writeResponse(logger, w, map[string]any{"stdout": "Credential deleted successfully"})
176+
}

pkg/sdkserver/routes.go

+5
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ func (s *server) addRoutes(mux *http.ServeMux) {
5858
mux.HandleFunc("POST /confirm/{id}", s.confirm)
5959
mux.HandleFunc("POST /prompt/{id}", s.prompt)
6060
mux.HandleFunc("POST /prompt-response/{id}", s.promptResponse)
61+
62+
mux.HandleFunc("POST /credentials", s.listCredentials)
63+
mux.HandleFunc("POST /credentials/create", s.createCredential)
64+
mux.HandleFunc("POST /credentials/reveal", s.revealCredential)
65+
mux.HandleFunc("POST /credentials/delete", s.deleteCredential)
6166
}
6267

6368
// health just provides an endpoint for checking whether the server is running and accessible.

pkg/sdkserver/types.go

+7
Original file line numberDiff line numberDiff line change
@@ -252,3 +252,10 @@ type prompt struct {
252252
Type runner.EventType `json:"type,omitempty"`
253253
Time time.Time `json:"time,omitempty"`
254254
}
255+
256+
type credentialsRequest struct {
257+
content `json:",inline"`
258+
AllContexts bool `json:"allContexts"`
259+
Context string `json:"context"`
260+
Name string `json:"name"`
261+
}

0 commit comments

Comments
 (0)