Skip to content

Commit 6c87a23

Browse files
g-linvillenjhale
andauthored
enhance: add credential overrides (#263)
Signed-off-by: Grant Linville <[email protected]> Co-authored-by: Nick Hale <[email protected]>
1 parent 7008513 commit 6c87a23

File tree

5 files changed

+187
-25
lines changed

5 files changed

+187
-25
lines changed

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

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,54 @@ You can delete a credential by running the following command:
9999
```bash
100100
gptscript credential delete --credential-context <credential context> <credential tool name>
101101
```
102+
103+
The `--show-env-vars` argument will also display the names of the environment variables that are set by the credential.
104+
This is useful when working with credential overrides.
105+
106+
## Credential Overrides
107+
108+
You can bypass credential tools and stored credentials by setting the `--credential-override` argument (or the
109+
`GPTSCRIPT_CREDENTIAL_OVERRIDE` environment variable) when running GPTScript. To set up a credential override, you
110+
need to be aware of which environment variables the credential tool sets. You can find this out by running the
111+
`gptscript credential --show-env-vars` command.
112+
113+
### Format
114+
115+
The `--credential-override` argument must be formatted in one of the following three ways:
116+
117+
#### 1. Key-Value Pairs
118+
119+
`toolA:ENV_VAR_1=value1,ENV_VAR_2=value2;toolB:ENV_VAR_1=value3,ENV_VAR_2=value4`
120+
121+
In this example, both `toolA` and `toolB` provide the variables `ENV_VAR_1` and `ENV_VAR_2`.
122+
This will set the environment variables `ENV_VAR_1` and `ENV_VAR_2` to the specific values provided for each tool.
123+
124+
#### 2. Environment Variables
125+
126+
`toolA:ENV_VAR_1,ENV_VAR_2;toolB:ENV_VAR_3,ENV_VAR_4`
127+
128+
In this example, `toolA` provides the variables `ENV_VAR_1` and `ENV_VAR_2`, and `toolB` provides the variables `ENV_VAR_3` and `ENV_VAR_4`.
129+
This will read the values of `ENV_VAR_1` through `ENV_VAR_4` from the current environment and set them for each tool.
130+
This is a direct mapping of environment variable names. **This is not recommended when overriding credentials for
131+
multiple tools that use the same environment variable names.**
132+
133+
#### 3. Environment Variable Mapping
134+
135+
`toolA:ENV_VAR_1->TOOL_A_ENV_VAR_1,ENV_VAR_2->TOOL_A_ENV_VAR_2;toolB:ENV_VAR_1->TOOL_B_ENV_VAR_1,ENV_VAR_2->TOOL_B_ENV_VAR_2`
136+
137+
In this example, `toolA` and `toolB` both provide the variables `ENV_VAR_1` and `ENV_VAR_2`.
138+
This will set the environment variables `ENV_VAR_1` and `ENV_VAR_2` to the values of `TOOL_A_ENV_VAR_1` and
139+
`TOOL_A_ENV_VAR_2` from the current environment for `toolA`. The same applies for `toolB`, but with the values of
140+
`TOOL_B_ENV_VAR_1` and `TOOL_B_ENV_VAR_2`. This is a mapping of one environment variable name to another.
141+
142+
### Real-World Example
143+
144+
Here is an example of how you can use a credential override to skip running the credential tool for the Brave Search tool:
145+
146+
```bash
147+
gptscript --credential-override "github.com/gptscript-ai/search/brave-credential:GPTSCRIPT_BRAVE_SEARCH_TOKEN->MY_BRAVE_SEARCH_TOKEN" github.com/gptscript-ai/search/brave '{"q": "cute cats"}'
148+
```
149+
150+
If you run this command, rather than being prompted by the credential tool for your token, GPTScript will read the contents
151+
of the environment variable `MY_BRAVE_SEARCH_TOKEN` and set that as the variable `GPTSCRIPT_BRAVE_SEARCH_TOKEN` when it runs
152+
the script.

pkg/cli/credential.go

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"os"
66
"sort"
7+
"strings"
78
"text/tabwriter"
89

910
cmd2 "github.com/acorn-io/cmd"
@@ -15,6 +16,7 @@ import (
1516
type Credential struct {
1617
root *GPTScript
1718
AllContexts bool `usage:"List credentials for all contexts" local:"true"`
19+
ShowEnvVars bool `usage:"Show names of environment variables in each credential" local:"true"`
1820
}
1921

2022
func (c *Credential) Customize(cmd *cobra.Command) {
@@ -57,18 +59,47 @@ func (c *Credential) Run(_ *cobra.Command, _ []string) error {
5759

5860
w := tabwriter.NewWriter(os.Stdout, 10, 1, 3, ' ', 0)
5961
defer w.Flush()
60-
_, _ = w.Write([]byte("CONTEXT\tTOOL\n"))
61-
for _, cred := range creds {
62-
_, _ = fmt.Fprintf(w, "%s\t%s\n", cred.Context, cred.ToolName)
62+
63+
if c.ShowEnvVars {
64+
_, _ = w.Write([]byte("CONTEXT\tTOOL\tENVIRONMENT VARIABLES\n"))
65+
66+
for _, cred := range creds {
67+
envVars := make([]string, 0, len(cred.Env))
68+
for envVar := range cred.Env {
69+
envVars = append(envVars, envVar)
70+
}
71+
sort.Strings(envVars)
72+
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\n", cred.Context, cred.ToolName, strings.Join(envVars, ", "))
73+
}
74+
} else {
75+
_, _ = w.Write([]byte("CONTEXT\tTOOL\n"))
76+
for _, cred := range creds {
77+
_, _ = fmt.Fprintf(w, "%s\t%s\n", cred.Context, cred.ToolName)
78+
}
6379
}
6480
} else {
6581
// Sort credentials by tool name
6682
sort.Slice(creds, func(i, j int) bool {
6783
return creds[i].ToolName < creds[j].ToolName
6884
})
6985

70-
for _, cred := range creds {
71-
fmt.Println(cred.ToolName)
86+
if c.ShowEnvVars {
87+
w := tabwriter.NewWriter(os.Stdout, 10, 1, 3, ' ', 0)
88+
defer w.Flush()
89+
_, _ = w.Write([]byte("TOOL\tENVIRONMENT VARIABLES\n"))
90+
91+
for _, cred := range creds {
92+
envVars := make([]string, 0, len(cred.Env))
93+
for envVar := range cred.Env {
94+
envVars = append(envVars, envVar)
95+
}
96+
sort.Strings(envVars)
97+
_, _ = fmt.Fprintf(w, "%s\t%s\n", cred.ToolName, strings.Join(envVars, ", "))
98+
}
99+
} else {
100+
for _, cred := range creds {
101+
fmt.Println(cred.ToolName)
102+
}
72103
}
73104
}
74105

pkg/cli/gptscript.go

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -39,22 +39,23 @@ type GPTScript struct {
3939
CacheOptions
4040
OpenAIOptions
4141
DisplayOptions
42-
Color *bool `usage:"Use color in output (default true)" default:"true"`
43-
Confirm bool `usage:"Prompt before running potentially dangerous commands"`
44-
Debug bool `usage:"Enable debug logging"`
45-
Quiet *bool `usage:"No output logging (set --quiet=false to force on even when there is no TTY)" short:"q"`
46-
Output string `usage:"Save output to a file, or - for stdout" short:"o"`
47-
Input string `usage:"Read input from a file (\"-\" for stdin)" short:"f"`
48-
SubTool string `usage:"Use tool of this name, not the first tool in file" local:"true"`
49-
Assemble bool `usage:"Assemble tool to a single artifact, saved to --output" hidden:"true" local:"true"`
50-
ListModels bool `usage:"List the models available and exit" local:"true"`
51-
ListTools bool `usage:"List built-in tools and exit" local:"true"`
52-
Server bool `usage:"Start server" local:"true"`
53-
ListenAddress string `usage:"Server listen address" default:"127.0.0.1:9090" local:"true"`
54-
Chdir string `usage:"Change current working directory" short:"C"`
55-
Daemon bool `usage:"Run tool as a daemon" local:"true" hidden:"true"`
56-
Ports string `usage:"The port range to use for ephemeral daemon ports (ex: 11000-12000)" hidden:"true"`
57-
CredentialContext string `usage:"Context name in which to store credentials" default:"default"`
42+
Color *bool `usage:"Use color in output (default true)" default:"true"`
43+
Confirm bool `usage:"Prompt before running potentially dangerous commands"`
44+
Debug bool `usage:"Enable debug logging"`
45+
Quiet *bool `usage:"No output logging (set --quiet=false to force on even when there is no TTY)" short:"q"`
46+
Output string `usage:"Save output to a file, or - for stdout" short:"o"`
47+
Input string `usage:"Read input from a file (\"-\" for stdin)" short:"f"`
48+
SubTool string `usage:"Use tool of this name, not the first tool in file" local:"true"`
49+
Assemble bool `usage:"Assemble tool to a single artifact, saved to --output" hidden:"true" local:"true"`
50+
ListModels bool `usage:"List the models available and exit" local:"true"`
51+
ListTools bool `usage:"List built-in tools and exit" local:"true"`
52+
Server bool `usage:"Start server" local:"true"`
53+
ListenAddress string `usage:"Server listen address" default:"127.0.0.1:9090" local:"true"`
54+
Chdir string `usage:"Change current working directory" short:"C"`
55+
Daemon bool `usage:"Run tool as a daemon" local:"true" hidden:"true"`
56+
Ports string `usage:"The port range to use for ephemeral daemon ports (ex: 11000-12000)" hidden:"true"`
57+
CredentialContext string `usage:"Context name in which to store credentials" default:"default"`
58+
CredentialOverride string `usage:"Credentials to override (ex: --credential-override github.com/example/cred-tool:API_TOKEN=1234)"`
5859
}
5960

6061
func New() *cobra.Command {
@@ -134,6 +135,8 @@ func (r *GPTScript) NewGPTScriptOpts() (gptscript.Options, error) {
134135
opts.Runner.EndPort = endNum
135136
}
136137

138+
opts.Runner.CredentialOverride = r.CredentialOverride
139+
137140
return opts, nil
138141
}
139142

pkg/runner/credentials.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package runner
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"strings"
7+
)
8+
9+
// parseCredentialOverrides parses a string of credential overrides that the user provided as a command line arg.
10+
// The format of credential overrides can be one of three things:
11+
// tool1:ENV1,ENV2;tool2:ENV1,ENV2 (direct mapping of environment variables)
12+
// tool1:ENV1=VALUE1,ENV2=VALUE2;tool2:ENV1=VALUE1,ENV2=VALUE2 (key-value pairs)
13+
// tool1:ENV1->OTHER_ENV1,ENV2->OTHER_ENV2;tool2:ENV1->OTHER_ENV1,ENV2->OTHER_ENV2 (mapping to other environment variables)
14+
//
15+
// This function turns it into a map[string]map[string]string like this:
16+
//
17+
// {
18+
// "tool1": {
19+
// "ENV1": "VALUE1",
20+
// "ENV2": "VALUE2",
21+
// },
22+
// "tool2": {
23+
// "ENV1": "VALUE1",
24+
// "ENV2": "VALUE2",
25+
// },
26+
// }
27+
func parseCredentialOverrides(override string) (map[string]map[string]string, error) {
28+
credentialOverrides := make(map[string]map[string]string)
29+
30+
for _, o := range strings.Split(override, ";") {
31+
toolName, envs, found := strings.Cut(o, ":")
32+
if !found {
33+
return nil, fmt.Errorf("invalid credential override: %s", o)
34+
}
35+
envMap := make(map[string]string)
36+
for _, env := range strings.Split(envs, ",") {
37+
key, value, found := strings.Cut(env, "=")
38+
if !found {
39+
var envVar string
40+
key, envVar, found = strings.Cut(env, "->")
41+
if found {
42+
// User did a mapping of key -> other env var, so look up the value.
43+
value = os.Getenv(envVar)
44+
} else {
45+
// User just passed an env var name as the key, so look up the value.
46+
value = os.Getenv(key)
47+
}
48+
}
49+
envMap[key] = value
50+
}
51+
credentialOverrides[toolName] = envMap
52+
}
53+
54+
return credentialOverrides, nil
55+
}

pkg/runner/runner.go

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,11 @@ type Monitor interface {
3030
type MonitorKey struct{}
3131

3232
type Options struct {
33-
MonitorFactory MonitorFactory `usage:"-"`
34-
RuntimeManager engine.RuntimeManager `usage:"-"`
35-
StartPort int64 `usage:"-"`
36-
EndPort int64 `usage:"-"`
33+
MonitorFactory MonitorFactory `usage:"-"`
34+
RuntimeManager engine.RuntimeManager `usage:"-"`
35+
StartPort int64 `usage:"-"`
36+
EndPort int64 `usage:"-"`
37+
CredentialOverride string `usage:"-"`
3738
}
3839

3940
func complete(opts ...Options) (result Options) {
@@ -42,6 +43,7 @@ func complete(opts ...Options) (result Options) {
4243
result.RuntimeManager = types.FirstSet(opt.RuntimeManager, result.RuntimeManager)
4344
result.StartPort = types.FirstSet(opt.StartPort, result.StartPort)
4445
result.EndPort = types.FirstSet(opt.EndPort, result.EndPort)
46+
result.CredentialOverride = types.FirstSet(opt.CredentialOverride, result.CredentialOverride)
4547
}
4648
if result.MonitorFactory == nil {
4749
result.MonitorFactory = noopFactory{}
@@ -62,6 +64,7 @@ type Runner struct {
6264
ports engine.Ports
6365
credCtx string
6466
credMutex sync.Mutex
67+
credOverrides string
6568
}
6669

6770
func New(client engine.Model, credCtx string, opts ...Options) (*Runner, error) {
@@ -73,6 +76,7 @@ func New(client engine.Model, credCtx string, opts ...Options) (*Runner, error)
7376
runtimeManager: opt.RuntimeManager,
7477
credCtx: credCtx,
7578
credMutex: sync.Mutex{},
79+
credOverrides: opt.CredentialOverride,
7680
}
7781

7882
if opt.StartPort != 0 {
@@ -336,6 +340,7 @@ func (r *Runner) handleCredentials(callCtx engine.Context, monitor Monitor, env
336340
r.credMutex.Lock()
337341
defer r.credMutex.Unlock()
338342

343+
// Set up the credential store.
339344
c, err := config.ReadCLIConfig("")
340345
if err != nil {
341346
return nil, fmt.Errorf("failed to read CLI config: %w", err)
@@ -346,7 +351,24 @@ func (r *Runner) handleCredentials(callCtx engine.Context, monitor Monitor, env
346351
return nil, fmt.Errorf("failed to create credentials store: %w", err)
347352
}
348353

354+
// Parse the credential overrides from the command line argument, if there are any.
355+
var credOverrides map[string]map[string]string
356+
if r.credOverrides != "" {
357+
credOverrides, err = parseCredentialOverrides(r.credOverrides)
358+
if err != nil {
359+
return nil, fmt.Errorf("failed to parse credential overrides: %w", err)
360+
}
361+
}
362+
349363
for _, credToolName := range callCtx.Tool.Credentials {
364+
// Check whether the credential was overridden before we attempt to find it in the store or run the tool.
365+
if override, exists := credOverrides[credToolName]; exists {
366+
for k, v := range override {
367+
env = append(env, fmt.Sprintf("%s=%s", k, v))
368+
}
369+
continue
370+
}
371+
350372
var (
351373
cred *credentials.Credential
352374
exists bool

0 commit comments

Comments
 (0)