Skip to content

Commit

Permalink
set up cred helpers
Browse files Browse the repository at this point in the history
Signed-off-by: Grant Linville <[email protected]>
  • Loading branch information
g-linville committed Jun 10, 2024
1 parent 21f0fea commit 9b2d6dc
Show file tree
Hide file tree
Showing 16 changed files with 322 additions and 106 deletions.
71 changes: 71 additions & 0 deletions docs/docs/02-credentials.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Credentials

Some GPTScript tools will use [credential tools](03-tools/04-credential-tools.md) to get sensitive information like API keys from the user.
These credentials will be stored in a credential store and are used to set environment variables before executing a tool.

GPTScript itself will also prompt you for your OpenAI API key and save it in the credential store if the
`OPENAI_API_KEY` environment variable is not set. The environment variable always overrides the value stored in the
credential store.

## Credential Store

There are different options available for credential stores, depending on your operating system.
When you first run GPTScript, the default credential store for your operating system will be selected.

You can change the credential store by modifying the `credsStore` field in your GPTScript configuration file.
The configuration file is located in the following location based on your operating system:
- Windows: `%APPDATA%\Local\gptscript\config.json`
- macOS: `$HOME/Library/Application Support/gptscript/config.json`
- Linux: `$XDG_CONFIG_HOME/gptscript/config.json`

(Note: if you set the `XDG_CONFIG_HOME` environment variable on macOS, then the same path as Linux will be used.)

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)
repository, other than the `file` store, which is built-in to GPTScript itself.
The `wincred` and `osxkeychain` stores do not require any external dependencies in order to compile correctly.
The `secretservice` store on Linux may require some extra packages to be installed, depending on your distribution.

## Credential Store Options

### Wincred (Windows)

Wincred, or the Windows Credential Manager, is the default credential store for Windows.
This is Windows' built-in credential manager that securely stores credentials for Windows applications.
This credential store is called `wincred` in GPTScript's configuration.

### macOS Keychain (macOS)

The macOS Keychain is the default credential store for macOS.
This is macOS' built-in password manager that securely stores credentials for macOS applications.
This credential store is called `osxkeychain` in GPTScript's configuration.

### File (all operating systems)

"File" is the default credential store for every other operating system besides Windows and macOS, but it
can also be configured on Windows and macOS. This will store credentials **unencrypted** inside GPTScript's
configuration file.
This credential store is called `file` in GPTScript's configuration.

### D-Bus Secret Service (Linux)

The D-Bus Secret Service can be used as the credential store for Linux systems with a desktop environment that supports it.
This credential store is called `secretservice` in GPTScript's configuration.

### Pass (Linux)

Pass can be used as the credential store for Linux systems. This requires the `pass` package to be installed
and configured. See [this guide](https://www.howtogeek.com/devops/how-to-use-pass-a-command-line-password-manager-for-linux-systems/)
for information about how to set it up.
This credential store is called `pass` in GPTScript's configuration.

## GPTScript `credential` Command

The `gptscript credential` command can be used to interact with your stored credentials.
`gptscript credential` without any arguments will list all stored credentials.
`gptscript credential delete <credential name>` will delete the specified credential, and you will be
prompted to enter it again the next time a tool that requires it is run.

## See Also

For more advanced credential usage, including credential contexts, writing credential tools, and using
credential tools, see [the credential tools documentation](03-tools/04-credential-tools).
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Credentials
# Credential Tools

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

## Storing Credentials

By default, credentials are automatically stored in a config file at `$XDG_CONFIG_HOME/gptscript/config.json`.
This config file also has another parameter, `credsStore`, which indicates where the credentials are being stored.

- `file` (default): The credentials are stored directly in the config file.
- `osxkeychain`: The credentials are stored in the macOS Keychain.
- `wincred`: The credentials are stored in the Windows Credential Manager.

In order to use `osxkeychain` or `wincred` as the credsStore, you must have the `gptscript-credential-*` executable
available in your PATH. There will probably be better packaging for this in the future, but for now, you can build them
from the [repo](https://github.com/gptscript-ai/gptscript-credential-helpers). (For wincred, make sure the executable
is called `gptscript-credential-wincred.exe`.)

There will likely be support added for other credential stores in the future.

:::note
Credentials received from credential provider tools that are not on GitHub (such as a local file) and do not have an alias
will not be stored in the credentials store.
:::
By default, credentials are automatically stored in the credential store. Read the [main credentials page](../02-credentials.md)
for more information about the credential store.

## Credential Aliases

Expand All @@ -112,7 +96,7 @@ or when you want to store credentials that were provided by a tool that is not o
## Credential Contexts

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.
A credential context is basically a namespace for credentials. If you have multiple credentials from the same provider tool,
A credential context is basically a namespace for credentials. If you have multiple credentials from the same provider tool,
you can switch between them by defining them in different credential contexts. The default context is called `default`,
and this is used if none is specified.

Expand Down
9 changes: 8 additions & 1 deletion pkg/cli/credential.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"text/tabwriter"

cmd2 "github.com/acorn-io/cmd"
"github.com/gptscript-ai/gptscript/pkg/cache"
"github.com/gptscript-ai/gptscript/pkg/config"
"github.com/gptscript-ai/gptscript/pkg/credentials"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -38,7 +39,13 @@ func (c *Credential) Run(_ *cobra.Command, _ []string) error {
ctx = "*"
}

store, err := credentials.NewStore(cfg, ctx)
opts, err := c.root.NewGPTScriptOpts()
if err != nil {
return err
}
opts.Cache = cache.Complete(opts.Cache)

store, err := credentials.NewStore(cfg, ctx, opts.Cache.CacheDir)
if err != nil {
return fmt.Errorf("failed to get credentials store: %w", err)
}
Expand Down
9 changes: 8 additions & 1 deletion pkg/cli/credential_delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cli
import (
"fmt"

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

func (c *Delete) Run(_ *cobra.Command, args []string) error {
opts, err := c.root.NewGPTScriptOpts()
if err != nil {
return err
}
opts.Cache = cache.Complete(opts.Cache)

cfg, err := config.ReadCLIConfig(c.root.ConfigFile)
if err != nil {
return fmt.Errorf("failed to read CLI config: %w", err)
}

store, err := credentials.NewStore(cfg, c.root.CredentialContext)
store, err := credentials.NewStore(cfg, c.root.CredentialContext, opts.Cache.CacheDir)
if err != nil {
return fmt.Errorf("failed to get credentials store: %w", err)
}
Expand Down
60 changes: 50 additions & 10 deletions pkg/config/cliconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,24 @@ package config
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"runtime"
"slices"
"strings"
"sync"

"github.com/adrg/xdg"
"github.com/docker/cli/cli/config/types"
)

var (
darwinHelpers = []string{"osxkeychain", "file"}
windowsHelpers = []string{"wincred", "file"}
linuxHelpers = []string{"secretservice", "pass", "file"}
)

const GPTScriptHelperPrefix = "gptscript-credential-"

type AuthConfig types.AuthConfig
Expand Down Expand Up @@ -132,21 +139,54 @@ func ReadCLIConfig(gptscriptConfigFile string) (*CLIConfig, error) {
}

if result.CredentialsStore == "" {
result.setDefaultCredentialsStore()
if err := result.setDefaultCredentialsStore(); err != nil {
return nil, err
}
}

if !isValidCredentialHelper(result.CredentialsStore) {
errMsg := fmt.Sprintf("invalid credential store '%s'", result.CredentialsStore)
switch runtime.GOOS {
case "darwin":
errMsg += " (use 'osxkeychain' or 'file')"
case "windows":
errMsg += " (use 'wincred' or 'file')"
case "linux":
errMsg += " (use 'secretservice', 'pass', or 'file')"
default:
errMsg += " (use 'file')"
}
errMsg += fmt.Sprintf("\nPlease edit your config file at %s to fix this.", result.GPTScriptConfigFile)

return nil, errors.New(errMsg)
}

return result, nil
}

func (c *CLIConfig) setDefaultCredentialsStore() {
if runtime.GOOS == "darwin" {
// Check for the existence of the helper program
fullPath, err := exec.LookPath(GPTScriptHelperPrefix + "osxkeychain")
if err == nil && fullPath != "" {
c.CredentialsStore = "osxkeychain"
}
func (c *CLIConfig) setDefaultCredentialsStore() error {
switch runtime.GOOS {
case "darwin":
c.CredentialsStore = "osxkeychain"
case "windows":
c.CredentialsStore = "wincred"
default:
c.CredentialsStore = "file"
}
return c.Save()
}

func isValidCredentialHelper(helper string) bool {
switch runtime.GOOS {
case "darwin":
return slices.Contains(darwinHelpers, helper)
case "windows":
return slices.Contains(windowsHelpers, helper)
case "linux":
return slices.Contains(linuxHelpers, helper)
default:
return helper == "file"
}
c.CredentialsStore = "file"
}

func readFile(path string) ([]byte, error) {
Expand Down
20 changes: 15 additions & 5 deletions pkg/credentials/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,28 @@ package credentials

import (
"fmt"
"path/filepath"
"regexp"
"strings"

"github.com/docker/cli/cli/config/credentials"
"github.com/gptscript-ai/gptscript/pkg/config"
)

type Store struct {
credCtx string
cfg *config.CLIConfig
credCtx string
credHelperDirs CredentialHelperDirs
cfg *config.CLIConfig
}

func NewStore(cfg *config.CLIConfig, credCtx string) (*Store, error) {
func NewStore(cfg *config.CLIConfig, credCtx, cacheDir string) (*Store, error) {
if err := validateCredentialCtx(credCtx); err != nil {
return nil, err
}
return &Store{
credCtx: credCtx,
cfg: cfg,
credCtx: credCtx,
credHelperDirs: GetCredentialHelperDirs(cacheDir),
cfg: cfg,
}, nil
}

Expand Down Expand Up @@ -103,6 +107,12 @@ func (s *Store) getStoreByHelper(helper string) (credentials.Store, error) {
if helper == "" || helper == config.GPTScriptHelperPrefix+"file" {
return credentials.NewFileStore(s.cfg), nil
}

// If the helper is referencing one of the credential helper programs, then reference the full path.
if strings.HasPrefix(helper, "gptscript-credential-") {
helper = filepath.Join(s.credHelperDirs.BinDir, helper)
}

return NewHelper(s.cfg, helper)
}

Expand Down
17 changes: 17 additions & 0 deletions pkg/credentials/util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package credentials

import (
"path/filepath"
)

type CredentialHelperDirs struct {
RevisionFile, BinDir, RepoDir string
}

func GetCredentialHelperDirs(cacheDir string) CredentialHelperDirs {
return CredentialHelperDirs{
RevisionFile: filepath.Join(cacheDir, "repos", "gptscript-credential-helpers", "revision"),
BinDir: filepath.Join(cacheDir, "repos", "gptscript-credential-helpers", "bin"),
RepoDir: filepath.Join(cacheDir, "repos", "gptscript-credential-helpers", "repo"),
}
}
2 changes: 2 additions & 0 deletions pkg/engine/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"strings"
"sync"

"github.com/gptscript-ai/gptscript/pkg/config"
"github.com/gptscript-ai/gptscript/pkg/counter"
"github.com/gptscript-ai/gptscript/pkg/system"
"github.com/gptscript-ai/gptscript/pkg/types"
Expand All @@ -19,6 +20,7 @@ type Model interface {

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

type Engine struct {
Expand Down
24 changes: 17 additions & 7 deletions pkg/gptscript/gptscript.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/gptscript-ai/gptscript/pkg/cache"
"github.com/gptscript-ai/gptscript/pkg/config"
context2 "github.com/gptscript-ai/gptscript/pkg/context"
"github.com/gptscript-ai/gptscript/pkg/credentials"
"github.com/gptscript-ai/gptscript/pkg/engine"
"github.com/gptscript-ai/gptscript/pkg/hash"
"github.com/gptscript-ai/gptscript/pkg/llm"
Expand Down Expand Up @@ -79,7 +80,20 @@ func New(opts *Options) (*GPTScript, error) {
return nil, err
}

oaiClient, err := openai.NewClient(cliCfg, opts.CredentialContext, opts.OpenAI, openai.Options{
credStore, err := credentials.NewStore(cliCfg, opts.CredentialContext, cacheClient.CacheDir())
if err != nil {
return nil, err
}

if opts.Runner.RuntimeManager == nil {
opts.Runner.RuntimeManager = runtimes.Default(cacheClient.CacheDir())
}

if err := opts.Runner.RuntimeManager.SetUpCredentialHelpers(context.Background(), cliCfg, opts.Env); err != nil {
return nil, err
}

oaiClient, err := openai.NewClient(credStore, opts.OpenAI, openai.Options{
Cache: cacheClient,
SetSeed: true,
})
Expand All @@ -95,11 +109,7 @@ func New(opts *Options) (*GPTScript, error) {
opts.Runner.MonitorFactory = monitor.NewConsole(opts.Monitor, monitor.Options{DebugMessages: *opts.Quiet})
}

if opts.Runner.RuntimeManager == nil {
opts.Runner.RuntimeManager = runtimes.Default(cacheClient.CacheDir())
}

runner, err := runner.New(registry, opts.CredentialContext, opts.Runner)
runner, err := runner.New(registry, credStore, opts.Runner)
if err != nil {
return nil, err
}
Expand All @@ -114,7 +124,7 @@ func New(opts *Options) (*GPTScript, error) {
fullEnv := append(opts.Env, extraEnv...)
oaiClient.SetEnvs(fullEnv)

remoteClient := remote.New(runner, fullEnv, cacheClient, cliCfg, opts.CredentialContext)
remoteClient := remote.New(runner, fullEnv, cacheClient, credStore)
if err := registry.AddClient(remoteClient); err != nil {
closeServer()
return nil, err
Expand Down
Loading

0 comments on commit 9b2d6dc

Please sign in to comment.