Skip to content

Commit 7fc884c

Browse files
authored
enhance: automatically set up credential helpers (#468)
Signed-off-by: Grant Linville <[email protected]>
1 parent bc95fb1 commit 7fc884c

File tree

17 files changed

+356
-106
lines changed

17 files changed

+356
-106
lines changed

docs/docs/02-credentials.md

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Credentials
2+
3+
Some GPTScript tools will use [credential tools](03-tools/04-credential-tools.md) to get sensitive information like API keys from the user.
4+
These credentials will be stored in a credential store and are used to set environment variables before executing a tool.
5+
6+
GPTScript itself will also prompt you for your OpenAI API key and save it in the credential store if the
7+
`OPENAI_API_KEY` environment variable is not set. The environment variable always overrides the value stored in the
8+
credential store.
9+
10+
## Credential Store
11+
12+
There are different options available for credential stores, depending on your operating system.
13+
When you first run GPTScript, the default credential store for your operating system will be selected.
14+
15+
You can change the credential store by modifying the `credsStore` field in your GPTScript configuration file.
16+
The configuration file is located in the following location based on your operating system:
17+
- Windows: `%APPDATA%\Local\gptscript\config.json`
18+
- macOS: `$HOME/Library/Application Support/gptscript/config.json`
19+
- Linux: `$XDG_CONFIG_HOME/gptscript/config.json`
20+
21+
(Note: if you set the `XDG_CONFIG_HOME` environment variable on macOS, then the same path as Linux will be used.)
22+
23+
The configured credential store will be automatically downloaded and compiled from the [gptscript-ai/gptscript-credential-helpers](https://github.com/gptscript-ai/gptscript-credential-helpers)
24+
repository, other than the `file` store, which is built-in to GPTScript itself.
25+
The `wincred` and `osxkeychain` stores do not require any external dependencies in order to compile correctly.
26+
The `secretservice` store on Linux may require some extra packages to be installed, depending on your distribution.
27+
28+
## Credential Store Options
29+
30+
### Wincred (Windows)
31+
32+
Wincred, or the Windows Credential Manager, is the default credential store for Windows.
33+
This is Windows' built-in credential manager that securely stores credentials for Windows applications.
34+
This credential store is called `wincred` in GPTScript's configuration.
35+
36+
### macOS Keychain (macOS)
37+
38+
The macOS Keychain is the default credential store for macOS.
39+
This is macOS' built-in password manager that securely stores credentials for macOS applications.
40+
This credential store is called `osxkeychain` in GPTScript's configuration.
41+
42+
### File (all operating systems)
43+
44+
"File" is the default credential store for every other operating system besides Windows and macOS, but it
45+
can also be configured on Windows and macOS. This will store credentials **unencrypted** inside GPTScript's
46+
configuration file.
47+
This credential store is called `file` in GPTScript's configuration.
48+
49+
### D-Bus Secret Service (Linux)
50+
51+
The D-Bus Secret Service can be used as the credential store for Linux systems with a desktop environment that supports it.
52+
This credential store is called `secretservice` in GPTScript's configuration.
53+
54+
### Pass (Linux)
55+
56+
Pass can be used as the credential store for Linux systems. This requires the `pass` package to be installed
57+
and configured. See [this guide](https://www.howtogeek.com/devops/how-to-use-pass-a-command-line-password-manager-for-linux-systems/)
58+
for information about how to set it up.
59+
This credential store is called `pass` in GPTScript's configuration.
60+
61+
## GPTScript `credential` Command
62+
63+
The `gptscript credential` command can be used to interact with your stored credentials.
64+
`gptscript credential` without any arguments will list all stored credentials.
65+
`gptscript credential delete <credential name>` will delete the specified credential, and you will be
66+
prompted to enter it again the next time a tool that requires it is run.
67+
68+
## See Also
69+
70+
For more advanced credential usage, including credential contexts, writing credential tools, and using
71+
credential tools, see [the credential tools documentation](03-tools/04-credential-tools.md).

docs/docs/03-tools/04-credentials.md renamed to docs/docs/03-tools/04-credential-tools.md

+4-15
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Credentials
1+
# Credential Tools
22

33
GPTScript supports credential provider tools. These tools can be used to fetch credentials from a secure location (or
44
directly from user input) and conveniently set them in the environment before running a script.
@@ -92,19 +92,8 @@ In this example, the tool's output would be `{"env":{"MY_ENV_VAR":"my value"}}`
9292

9393
## Storing Credentials
9494

95-
By default, credentials are automatically stored in a config file at `$XDG_CONFIG_HOME/gptscript/config.json`.
96-
This config file also has another parameter, `credsStore`, which indicates where the credentials are being stored.
97-
98-
- `file` (default): The credentials are stored directly in the config file.
99-
- `osxkeychain`: The credentials are stored in the macOS Keychain.
100-
- `wincred`: The credentials are stored in the Windows Credential Manager.
101-
102-
In order to use `osxkeychain` or `wincred` as the credsStore, you must have the `gptscript-credential-*` executable
103-
available in your PATH. There will probably be better packaging for this in the future, but for now, you can build them
104-
from the [repo](https://github.com/gptscript-ai/gptscript-credential-helpers). (For wincred, make sure the executable
105-
is called `gptscript-credential-wincred.exe`.)
106-
107-
There will likely be support added for other credential stores in the future.
95+
By default, credentials are automatically stored in the credential store. Read the [main credentials page](../02-credentials.md)
96+
for more information about the credential store.
10897

10998
:::note
11099
Credentials received from credential provider tools that are not on GitHub (such as a local file) and do not have an alias
@@ -128,7 +117,7 @@ or when you want to store credentials that were provided by a tool that is not o
128117
## Credential Contexts
129118

130119
Each stored credential is uniquely identified by the name of its provider tool (or alias, if one was specified) and the name of its context.
131-
A credential context is basically a namespace for credentials. If you have multiple credentials from the same provider tool,
120+
A credential context is basically a namespace for credentials. If you have multiple credentials from the same provider tool,
132121
you can switch between them by defining them in different credential contexts. The default context is called `default`,
133122
and this is used if none is specified.
134123

pkg/cli/credential.go

+8-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"text/tabwriter"
99

1010
cmd2 "github.com/acorn-io/cmd"
11+
"github.com/gptscript-ai/gptscript/pkg/cache"
1112
"github.com/gptscript-ai/gptscript/pkg/config"
1213
"github.com/gptscript-ai/gptscript/pkg/credentials"
1314
"github.com/spf13/cobra"
@@ -38,7 +39,13 @@ func (c *Credential) Run(_ *cobra.Command, _ []string) error {
3839
ctx = "*"
3940
}
4041

41-
store, err := credentials.NewStore(cfg, ctx)
42+
opts, err := c.root.NewGPTScriptOpts()
43+
if err != nil {
44+
return err
45+
}
46+
opts.Cache = cache.Complete(opts.Cache)
47+
48+
store, err := credentials.NewStore(cfg, ctx, opts.Cache.CacheDir)
4249
if err != nil {
4350
return fmt.Errorf("failed to get credentials store: %w", err)
4451
}

pkg/cli/credential_delete.go

+8-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cli
33
import (
44
"fmt"
55

6+
"github.com/gptscript-ai/gptscript/pkg/cache"
67
"github.com/gptscript-ai/gptscript/pkg/config"
78
"github.com/gptscript-ai/gptscript/pkg/credentials"
89
"github.com/spf13/cobra"
@@ -21,12 +22,18 @@ func (c *Delete) Customize(cmd *cobra.Command) {
2122
}
2223

2324
func (c *Delete) Run(_ *cobra.Command, args []string) error {
25+
opts, err := c.root.NewGPTScriptOpts()
26+
if err != nil {
27+
return err
28+
}
29+
opts.Cache = cache.Complete(opts.Cache)
30+
2431
cfg, err := config.ReadCLIConfig(c.root.ConfigFile)
2532
if err != nil {
2633
return fmt.Errorf("failed to read CLI config: %w", err)
2734
}
2835

29-
store, err := credentials.NewStore(cfg, c.root.CredentialContext)
36+
store, err := credentials.NewStore(cfg, c.root.CredentialContext, opts.Cache.CacheDir)
3037
if err != nil {
3138
return fmt.Errorf("failed to get credentials store: %w", err)
3239
}

pkg/config/cliconfig.go

+50-10
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,24 @@ package config
33
import (
44
"encoding/base64"
55
"encoding/json"
6+
"errors"
67
"fmt"
78
"os"
8-
"os/exec"
99
"runtime"
10+
"slices"
1011
"strings"
1112
"sync"
1213

1314
"github.com/adrg/xdg"
1415
"github.com/docker/cli/cli/config/types"
1516
)
1617

18+
var (
19+
darwinHelpers = []string{"osxkeychain", "file"}
20+
windowsHelpers = []string{"wincred", "file"}
21+
linuxHelpers = []string{"secretservice", "pass", "file"}
22+
)
23+
1724
const GPTScriptHelperPrefix = "gptscript-credential-"
1825

1926
type AuthConfig types.AuthConfig
@@ -132,21 +139,54 @@ func ReadCLIConfig(gptscriptConfigFile string) (*CLIConfig, error) {
132139
}
133140

134141
if result.CredentialsStore == "" {
135-
result.setDefaultCredentialsStore()
142+
if err := result.setDefaultCredentialsStore(); err != nil {
143+
return nil, err
144+
}
145+
}
146+
147+
if !isValidCredentialHelper(result.CredentialsStore) {
148+
errMsg := fmt.Sprintf("invalid credential store '%s'", result.CredentialsStore)
149+
switch runtime.GOOS {
150+
case "darwin":
151+
errMsg += " (use 'osxkeychain' or 'file')"
152+
case "windows":
153+
errMsg += " (use 'wincred' or 'file')"
154+
case "linux":
155+
errMsg += " (use 'secretservice', 'pass', or 'file')"
156+
default:
157+
errMsg += " (use 'file')"
158+
}
159+
errMsg += fmt.Sprintf("\nPlease edit your config file at %s to fix this.", result.GPTScriptConfigFile)
160+
161+
return nil, errors.New(errMsg)
136162
}
137163

138164
return result, nil
139165
}
140166

141-
func (c *CLIConfig) setDefaultCredentialsStore() {
142-
if runtime.GOOS == "darwin" {
143-
// Check for the existence of the helper program
144-
fullPath, err := exec.LookPath(GPTScriptHelperPrefix + "osxkeychain")
145-
if err == nil && fullPath != "" {
146-
c.CredentialsStore = "osxkeychain"
147-
}
167+
func (c *CLIConfig) setDefaultCredentialsStore() error {
168+
switch runtime.GOOS {
169+
case "darwin":
170+
c.CredentialsStore = "osxkeychain"
171+
case "windows":
172+
c.CredentialsStore = "wincred"
173+
default:
174+
c.CredentialsStore = "file"
175+
}
176+
return c.Save()
177+
}
178+
179+
func isValidCredentialHelper(helper string) bool {
180+
switch runtime.GOOS {
181+
case "darwin":
182+
return slices.Contains(darwinHelpers, helper)
183+
case "windows":
184+
return slices.Contains(windowsHelpers, helper)
185+
case "linux":
186+
return slices.Contains(linuxHelpers, helper)
187+
default:
188+
return helper == "file"
148189
}
149-
c.CredentialsStore = "file"
150190
}
151191

152192
func readFile(path string) ([]byte, error) {

pkg/credentials/noop.go

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package credentials
2+
3+
type NoopStore struct{}
4+
5+
func (s NoopStore) Get(_ string) (*Credential, bool, error) {
6+
return nil, false, nil
7+
}
8+
9+
func (s NoopStore) Add(_ Credential) error {
10+
return nil
11+
}
12+
13+
func (s NoopStore) Remove(_ string) error {
14+
return nil
15+
}
16+
17+
func (s NoopStore) List() ([]Credential, error) {
18+
return nil, nil
19+
}

pkg/credentials/store.go

+27-10
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,39 @@ package credentials
22

33
import (
44
"fmt"
5+
"path/filepath"
56
"regexp"
7+
"strings"
68

79
"github.com/docker/cli/cli/config/credentials"
810
"github.com/gptscript-ai/gptscript/pkg/config"
911
)
1012

13+
type CredentialStore interface {
14+
Get(toolName string) (*Credential, bool, error)
15+
Add(cred Credential) error
16+
Remove(toolName string) error
17+
List() ([]Credential, error)
18+
}
19+
1120
type Store struct {
12-
credCtx string
13-
cfg *config.CLIConfig
21+
credCtx string
22+
credHelperDirs CredentialHelperDirs
23+
cfg *config.CLIConfig
1424
}
1525

16-
func NewStore(cfg *config.CLIConfig, credCtx string) (*Store, error) {
26+
func NewStore(cfg *config.CLIConfig, credCtx, cacheDir string) (CredentialStore, error) {
1727
if err := validateCredentialCtx(credCtx); err != nil {
1828
return nil, err
1929
}
20-
return &Store{
21-
credCtx: credCtx,
22-
cfg: cfg,
30+
return Store{
31+
credCtx: credCtx,
32+
credHelperDirs: GetCredentialHelperDirs(cacheDir),
33+
cfg: cfg,
2334
}, nil
2435
}
2536

26-
func (s *Store) Get(toolName string) (*Credential, bool, error) {
37+
func (s Store) Get(toolName string) (*Credential, bool, error) {
2738
store, err := s.getStore()
2839
if err != nil {
2940
return nil, false, err
@@ -46,7 +57,7 @@ func (s *Store) Get(toolName string) (*Credential, bool, error) {
4657
return &cred, true, nil
4758
}
4859

49-
func (s *Store) Add(cred Credential) error {
60+
func (s Store) Add(cred Credential) error {
5061
cred.Context = s.credCtx
5162
store, err := s.getStore()
5263
if err != nil {
@@ -59,15 +70,15 @@ func (s *Store) Add(cred Credential) error {
5970
return store.Store(auth)
6071
}
6172

62-
func (s *Store) Remove(toolName string) error {
73+
func (s Store) Remove(toolName string) error {
6374
store, err := s.getStore()
6475
if err != nil {
6576
return err
6677
}
6778
return store.Erase(toolNameWithCtx(toolName, s.credCtx))
6879
}
6980

70-
func (s *Store) List() ([]Credential, error) {
81+
func (s Store) List() ([]Credential, error) {
7182
store, err := s.getStore()
7283
if err != nil {
7384
return nil, err
@@ -103,6 +114,12 @@ func (s *Store) getStoreByHelper(helper string) (credentials.Store, error) {
103114
if helper == "" || helper == config.GPTScriptHelperPrefix+"file" {
104115
return credentials.NewFileStore(s.cfg), nil
105116
}
117+
118+
// If the helper is referencing one of the credential helper programs, then reference the full path.
119+
if strings.HasPrefix(helper, "gptscript-credential-") {
120+
helper = filepath.Join(s.credHelperDirs.BinDir, helper)
121+
}
122+
106123
return NewHelper(s.cfg, helper)
107124
}
108125

pkg/credentials/util.go

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package credentials
2+
3+
import (
4+
"path/filepath"
5+
)
6+
7+
type CredentialHelperDirs struct {
8+
RevisionFile, BinDir, RepoDir string
9+
}
10+
11+
func GetCredentialHelperDirs(cacheDir string) CredentialHelperDirs {
12+
return CredentialHelperDirs{
13+
RevisionFile: filepath.Join(cacheDir, "repos", "gptscript-credential-helpers", "revision"),
14+
BinDir: filepath.Join(cacheDir, "repos", "gptscript-credential-helpers", "bin"),
15+
RepoDir: filepath.Join(cacheDir, "repos", "gptscript-credential-helpers", "repo"),
16+
}
17+
}

pkg/engine/engine.go

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"strings"
88
"sync"
99

10+
"github.com/gptscript-ai/gptscript/pkg/config"
1011
"github.com/gptscript-ai/gptscript/pkg/counter"
1112
"github.com/gptscript-ai/gptscript/pkg/system"
1213
"github.com/gptscript-ai/gptscript/pkg/types"
@@ -19,6 +20,7 @@ type Model interface {
1920

2021
type RuntimeManager interface {
2122
GetContext(ctx context.Context, tool types.Tool, cmd, env []string) (string, []string, error)
23+
SetUpCredentialHelpers(ctx context.Context, cliCfg *config.CLIConfig, env []string) error
2224
}
2325

2426
type Engine struct {

0 commit comments

Comments
 (0)