Skip to content

Commit e418b25

Browse files
committed
feat: credentials framework
Signed-off-by: Grant Linville <[email protected]>
1 parent 5c6ff70 commit e418b25

File tree

24 files changed

+924
-65
lines changed

24 files changed

+924
-65
lines changed

docs/docs/03-tools/04-credentials.md

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# Credentials
2+
3+
GPTScript supports credential provider tools. These tools can be used to fetch credentials from a secure location (or
4+
directly from user input) and conveniently set them in the environment before running a script.
5+
6+
## Writing a Credential Provider Tool
7+
8+
A credential provider tool looks just like any other GPTScript, with the following caveats:
9+
- It cannot call the LLM and must run a command.
10+
- It must print contents to stdout in the format `{"env":{"ENV_VAR_1":"value1","ENV_VAR_2":"value2"}}`.
11+
- Any args defined on the tool will be ignored.
12+
13+
Here is a simple example of a credential provider tool that uses the builtin `sys.prompt` to ask the user for some input:
14+
15+
```yaml
16+
# my-credential-tool.gpt
17+
name: my-credential-tool
18+
19+
#!/usr/bin/env bash
20+
21+
output=$(gptscript -q --cache=false sys.prompt '{"message":"Please enter your fake credential.","fields":"credential"}')
22+
credential=$(echo $output | jq -r '.credential')
23+
echo "{\"env\":{\"MY_ENV_VAR\":\"$credential\"}}"
24+
```
25+
26+
## Using a Credential Provider Tool
27+
28+
Continuing with the above example, this is how you can use it in a script:
29+
30+
```yaml
31+
credentials: my-credential-tool.gpt
32+
33+
#!/usr/bin/env bash
34+
35+
echo "The value of MY_ENV_VAR is $MY_ENV_VAR"
36+
```
37+
38+
When you run the script, GPTScript will call the credential provider tool first, set the environment variables from its
39+
output, and then run the script body. The credential provider tool is called by GPTScript itself. GPTScript does not ask the
40+
LLM about it or even tell the LLM about the tool.
41+
42+
If GPTScript has called the credential provider tool in the same context (more on that later), then it will use the stored
43+
credential instead of fetching it again.
44+
45+
You can also specify multiple credential tools for the same script:
46+
47+
```yaml
48+
credentials: credential-tool-1.gpt, credential-tool-2.gpt
49+
50+
(tool stuff here)
51+
```
52+
53+
## Storing Credentials
54+
55+
By default, credentials are automatically stored in a config file at `$XDG_CONFIG_HOME/gptscript/config.json`.
56+
This config file also has another parameter, `credsStore`, which indicates where the credentials are being stored.
57+
58+
- `file` (default): The credentials are stored directly in the config file.
59+
- `osxkeychain`: The credentials are stored in the macOS Keychain.
60+
61+
In order to use `osxkeychain` as the credsStore, you must have the `gptscript-credential-osxkeychain` executable
62+
available in your PATH. There will probably be better packaging for this in the future, but for now, you can build it
63+
from the [repo](https://github.com/gptscript-ai/gptscript-credential-helpers).
64+
65+
There will likely be support added for other credential stores in the future.
66+
67+
:::note
68+
Credentials received from credential provider tools that are not on GitHub (such as a local file) will not be stored
69+
in the credentials store.
70+
:::
71+
72+
## Credential Contexts
73+
74+
Each stored credential is uniquely identified by the name of its provider tool and the name of its context. A credential
75+
context is basically a namespace for credentials. If you have multiple credentials from the same provider tool, you can
76+
switch between them by defining them in different credential contexts. The default context is called `default`, and this
77+
is used if none is specified.
78+
79+
You can set the credential context to use with the `--credential-context` or `-c` flag when running GPTScript. For
80+
example:
81+
82+
```bash
83+
gptscript -c my-azure-workspace my-azure-script.gpt
84+
```
85+
86+
Any credentials fetched for that script will be stored in the `my-azure-workspace` context. If you were to call it again
87+
with a different context, you would be able to give it a different set of credentials.
88+
89+
## Listing and Deleting Stored Credentials
90+
91+
The `gptscript credential` command can be used to list and delete stored credentials. Running the command with no
92+
`--credential-context` (or `-c`) set will use the `default` credential context. You can also specify that it should list
93+
credentials in all contexts with `--all-contexts` or `-A`.
94+
95+
You can delete a credential by running the following command:
96+
97+
```bash
98+
gptscript credential delete -c <credential context> <credential tool name>
99+
```

docs/docs/06-gpt-file-reference.md

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -43,17 +43,18 @@ Tool instructions go here.
4343

4444
Tool parameters are key-value pairs defined at the beginning of a tool block, before any instructional text. They are specified in the format `key: value`. The parser recognizes the following keys (case-insensitive and spaces are ignored):
4545

46-
| Key | Description |
47-
|------------------|-----------------------------------------------------------------------------------------------------------------------------------------|
48-
| `Name` | The name of the tool. |
49-
| `Model Name` | The OpenAI model to use, by default it uses "gpt-4-turbo-preview" |
50-
| `Description` | The description of the tool. It is important that this properly describes the tool's purpose as the description is used by the LLM. |
51-
| `Internal Prompt`| Setting this to `false` will disable the built-in system prompt for this tool. |
52-
| `Tools` | A comma-separated list of tools that are available to be called by this tool. |
53-
| `Args` | Arguments for the tool. Each argument is defined in the format `arg-name: description`. |
54-
| `Max Tokens` | Set to a number if you wish to limit the maximum number of tokens that can be generated by the LLM. |
55-
| `JSON Response` | Setting to `true` will cause the LLM to respond in a JSON format. If you set true you must also include instructions in the tool. |
56-
| `Temperature` | A floating-point number representing the temperature parameter. By default, the temperature is 0. Set to a higher number for more creativity. |
46+
| Key | Description |
47+
|-------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|
48+
| `Name` | The name of the tool. |
49+
| `Model Name` | The OpenAI model to use, by default it uses "gpt-4-turbo-preview" |
50+
| `Description` | The description of the tool. It is important that this properly describes the tool's purpose as the description is used by the LLM. |
51+
| `Internal Prompt` | Setting this to `false` will disable the built-in system prompt for this tool. |
52+
| `Tools` | A comma-separated list of tools that are available to be called by this tool. |
53+
| `Credentials` | A comma-separated list of credential tools to run before the main tool. |
54+
| `Args` | Arguments for the tool. Each argument is defined in the format `arg-name: description`. |
55+
| `Max Tokens` | Set to a number if you wish to limit the maximum number of tokens that can be generated by the LLM. |
56+
| `JSON Response` | Setting to `true` will cause the LLM to respond in a JSON format. If you set true you must also include instructions in the tool. |
57+
| `Temperature` | A floating-point number representing the temperature parameter. By default, the temperature is 0. Set to a higher number for more creativity. |
5758

5859

5960

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ require (
1010
github.com/acorn-io/broadcaster v0.0.0-20240105011354-bfadd4a7b45d
1111
github.com/acorn-io/cmd v0.0.0-20240203032901-e9e631185ddb
1212
github.com/adrg/xdg v0.4.0
13+
github.com/docker/cli v26.0.0+incompatible
14+
github.com/docker/docker-credential-helpers v0.8.1
1315
github.com/fatih/color v1.16.0
1416
github.com/getkin/kin-openapi v0.123.0
1517
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510

go.sum

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr
5555
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5656
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
5757
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
58+
github.com/docker/cli v26.0.0+incompatible h1:90BKrx1a1HKYpSnnBFR6AgDq/FqkHxwlUyzJVPxD30I=
59+
github.com/docker/cli v26.0.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
60+
github.com/docker/docker-credential-helpers v0.8.1 h1:j/eKUktUltBtMzKqmfLB0PAgqYyMHOp5vfsD1807oKo=
61+
github.com/docker/docker-credential-helpers v0.8.1/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
5862
github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=
5963
github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
6064
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
@@ -208,6 +212,8 @@ github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0V
208212
github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0=
209213
github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
210214
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
215+
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
216+
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
211217
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
212218
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
213219
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
@@ -451,6 +457,8 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
451457
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
452458
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
453459
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
460+
gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
461+
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
454462
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
455463
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
456464
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

pkg/builtin/builtin.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@ import (
1717
"strings"
1818
"time"
1919

20+
"github.com/AlecAivazis/survey/v2"
2021
"github.com/BurntSushi/locker"
2122
"github.com/google/shlex"
2223
"github.com/gptscript-ai/gptscript/pkg/confirm"
24+
"github.com/gptscript-ai/gptscript/pkg/runner"
2325
"github.com/gptscript-ai/gptscript/pkg/types"
2426
"github.com/jaytaylor/html2text"
2527
)
@@ -141,6 +143,17 @@ var tools = map[string]types.Tool{
141143
},
142144
BuiltinFunc: SysStat,
143145
},
146+
"sys.prompt": {
147+
Parameters: types.Parameters{
148+
Description: "Prompts the user for input",
149+
Arguments: types.ObjectSchema(
150+
"message", "The message to display to the user",
151+
"fields", "A comma-separated list of fields to prompt for",
152+
"sensitive", "(true or false) Whether the input should be hidden",
153+
),
154+
},
155+
BuiltinFunc: SysPrompt,
156+
},
144157
}
145158

146159
func SysProgram() *types.Program {
@@ -589,3 +602,47 @@ func SysDownload(ctx context.Context, env []string, input string) (_ string, err
589602

590603
return params.Location, nil
591604
}
605+
606+
func SysPrompt(ctx context.Context, _ []string, input string) (_ string, err error) {
607+
monitor := ctx.Value("monitor")
608+
if monitor == nil {
609+
return "", errors.New("no monitor in context")
610+
}
611+
612+
unpause := monitor.(runner.Monitor).Pause()
613+
defer unpause()
614+
615+
var params struct {
616+
Message string `json:"message,omitempty"`
617+
Fields string `json:"fields,omitempty"`
618+
Sensitive string `json:"sensitive,omitempty"`
619+
}
620+
if err := json.Unmarshal([]byte(input), &params); err != nil {
621+
return "", err
622+
}
623+
624+
if params.Message != "" {
625+
_, _ = fmt.Fprintln(os.Stderr, params.Message)
626+
}
627+
628+
results := map[string]string{}
629+
for _, f := range strings.Split(params.Fields, ",") {
630+
var value string
631+
if params.Sensitive == "true" {
632+
err = survey.AskOne(&survey.Password{Message: f}, &value, survey.WithStdio(os.Stdin, os.Stderr, os.Stderr))
633+
} else {
634+
err = survey.AskOne(&survey.Input{Message: f}, &value, survey.WithStdio(os.Stdin, os.Stderr, os.Stderr))
635+
}
636+
if err != nil {
637+
return "", err
638+
}
639+
results[f] = value
640+
}
641+
642+
resultsStr, err := json.Marshal(results)
643+
if err != nil {
644+
return "", err
645+
}
646+
647+
return string(resultsStr), nil
648+
}

pkg/cli/credential.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"sort"
7+
"text/tabwriter"
8+
9+
"github.com/acorn-io/cmd"
10+
"github.com/gptscript-ai/gptscript/pkg/config"
11+
"github.com/gptscript-ai/gptscript/pkg/credentials"
12+
"github.com/spf13/cobra"
13+
)
14+
15+
type Credential struct {
16+
root *GPTScript
17+
AllContexts bool `short:"A" usage:"List credentials for all contexts" local:"true"`
18+
}
19+
20+
func NewCredential(root *GPTScript) *cobra.Command {
21+
c := cmd.Command(&Credential{root: root}, cobra.Command{
22+
Use: "credential",
23+
Aliases: []string{"cred", "creds", "credentials"},
24+
Short: "List stored credentials",
25+
Args: cobra.NoArgs,
26+
})
27+
c.AddCommand(NewDelete(root))
28+
return c
29+
}
30+
31+
func (c *Credential) Run(_ *cobra.Command, _ []string) error {
32+
cfg, err := config.ReadCLIConfig(c.root.ConfigFile)
33+
if err != nil {
34+
return fmt.Errorf("failed to read CLI config: %w", err)
35+
}
36+
37+
ctx := c.root.CredentialContext
38+
if c.AllContexts {
39+
ctx = "*"
40+
}
41+
42+
store, err := credentials.NewStore(cfg, ctx)
43+
if err != nil {
44+
return fmt.Errorf("failed to get credentials store: %w", err)
45+
}
46+
47+
creds, err := store.List()
48+
if err != nil {
49+
return fmt.Errorf("failed to list credentials: %w", err)
50+
}
51+
52+
if c.AllContexts {
53+
// Sort credentials by context
54+
sort.Slice(creds, func(i, j int) bool {
55+
if creds[i].Context == creds[j].Context {
56+
return creds[i].ToolName < creds[j].ToolName
57+
}
58+
return creds[i].Context < creds[j].Context
59+
})
60+
61+
w := tabwriter.NewWriter(os.Stdout, 10, 1, 3, ' ', 0)
62+
defer w.Flush()
63+
_, _ = w.Write([]byte("CONTEXT\tTOOL\n"))
64+
for _, cred := range creds {
65+
_, _ = fmt.Fprintf(w, "%s\t%s\n", cred.Context, cred.ToolName)
66+
}
67+
} else {
68+
// Sort credentials by tool name
69+
sort.Slice(creds, func(i, j int) bool {
70+
return creds[i].ToolName < creds[j].ToolName
71+
})
72+
73+
for _, cred := range creds {
74+
fmt.Println(cred.ToolName)
75+
}
76+
}
77+
78+
return nil
79+
}

pkg/cli/credential_delete.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/acorn-io/cmd"
7+
"github.com/gptscript-ai/gptscript/pkg/config"
8+
"github.com/gptscript-ai/gptscript/pkg/credentials"
9+
"github.com/spf13/cobra"
10+
)
11+
12+
type Delete struct {
13+
root *GPTScript
14+
}
15+
16+
func NewDelete(root *GPTScript) *cobra.Command {
17+
return cmd.Command(&Delete{root: root}, cobra.Command{
18+
Use: "delete <tool name>",
19+
SilenceUsage: true,
20+
Short: "Delete a stored credential",
21+
Args: cobra.ExactArgs(1),
22+
})
23+
}
24+
25+
func (c *Delete) Run(_ *cobra.Command, args []string) error {
26+
cfg, err := config.ReadCLIConfig(c.root.ConfigFile)
27+
if err != nil {
28+
return fmt.Errorf("failed to read CLI config: %w", err)
29+
}
30+
31+
store, err := credentials.NewStore(cfg, c.root.NewGPTScriptOpts().CredentialContext)
32+
if err != nil {
33+
return fmt.Errorf("failed to get credentials store: %w", err)
34+
}
35+
36+
if err = store.Remove(args[0]); err != nil {
37+
return fmt.Errorf("failed to remove credential: %w", err)
38+
}
39+
return nil
40+
}

0 commit comments

Comments
 (0)