Skip to content

Commit 780e07e

Browse files
authored
feat: add stacked credential contexts (#849)
Signed-off-by: Grant Linville <[email protected]>
1 parent 45d444f commit 780e07e

21 files changed

+283
-81
lines changed

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

+52
Original file line numberDiff line numberDiff line change
@@ -222,3 +222,55 @@ import os
222222
223223
print("myCred expires at " + os.getenv("GPTSCRIPT_CREDENTIAL_EXPIRATION", ""))
224224
```
225+
226+
## Stacked Credential Contexts (Advanced)
227+
228+
When setting the `--credential-context` argument in GPTScript, you can specify multiple contexts separated by commas.
229+
We refer to this as "stacked credential contexts", or just stacked contexts for short. This allows you to specify an order
230+
of priority for credential contexts. This is best explained by example.
231+
232+
### Example: stacked contexts when running a script that uses a credential
233+
234+
Let's say you have two contexts, `one` and `two`, and you specify them like this:
235+
236+
```bash
237+
gptscript --credential-context one,two my-script.gpt
238+
```
239+
240+
```
241+
Credential: my-credential-tool.gpt as myCred
242+
243+
<tool stuff here>
244+
```
245+
246+
When GPTScript runs, it will first look for a credential called `myCred` in the `one` context.
247+
If it doesn't find it there, it will look for it in the `two` context. If it also doesn't find it there,
248+
it will run the `my-credential-tool.gpt` tool to get the credential. It will then store the new credential into the `one`
249+
context, since that has the highest priority.
250+
251+
### Example: stacked contexts when listing credentials
252+
253+
```bash
254+
gptscript --credential-context one,two credentials
255+
```
256+
257+
When you list credentials like this, GPTScript will print out the information for all credentials in contexts one and two,
258+
with one exception. If there is a credential name that exists in both contexts, GPTScript will only print the information
259+
for the credential in the context with the highest priority, which in this case is `one`.
260+
261+
(To see all credentials in all contexts, you can still use the `--all-contexts` flag, and it will show all credentials,
262+
regardless of whether the same name appears in another context.)
263+
264+
### Example: stacked contexts when showing credentials
265+
266+
```bash
267+
gptscript --credential-context one,two credential show myCred
268+
```
269+
270+
When you show a credential like this, GPTScript will first look for `myCred` in the `one` context. If it doesn't find it
271+
there, it will look for it in the `two` context. If it doesn't find it in either context, it will print an error message.
272+
273+
:::note
274+
You cannot specify stacked contexts when doing `gptscript credential delete`. GPTScript will return an error if
275+
more than one context is specified for this command.
276+
:::

docs/docs/04-command-line-reference/gptscript.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ gptscript [flags] PROGRAM_FILE [INPUT...]
1818
--color Use color in output (default true) ($GPTSCRIPT_COLOR)
1919
--config string Path to GPTScript config file ($GPTSCRIPT_CONFIG)
2020
--confirm Prompt before running potentially dangerous commands ($GPTSCRIPT_CONFIRM)
21-
--credential-context string Context name in which to store credentials ($GPTSCRIPT_CREDENTIAL_CONTEXT) (default "default")
21+
--credential-context strings Context name(s) in which to store credentials ($GPTSCRIPT_CREDENTIAL_CONTEXT)
2222
--credential-override strings Credentials to override (ex: --credential-override github.com/example/cred-tool:API_TOKEN=1234) ($GPTSCRIPT_CREDENTIAL_OVERRIDE)
2323
--debug Enable debug logging ($GPTSCRIPT_DEBUG)
2424
--debug-messages Enable logging of chat completion calls ($GPTSCRIPT_DEBUG_MESSAGES)

docs/docs/04-command-line-reference/gptscript_credential.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ gptscript credential [flags]
2020
### Options inherited from parent commands
2121

2222
```
23-
--credential-context string Context name in which to store credentials ($GPTSCRIPT_CREDENTIAL_CONTEXT) (default "default")
23+
--credential-context strings Context name(s) in which to store credentials ($GPTSCRIPT_CREDENTIAL_CONTEXT)
2424
```
2525

2626
### SEE ALSO

docs/docs/04-command-line-reference/gptscript_credential_delete.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ gptscript credential delete <credential name> [flags]
1818
### Options inherited from parent commands
1919

2020
```
21-
--credential-context string Context name in which to store credentials ($GPTSCRIPT_CREDENTIAL_CONTEXT) (default "default")
21+
--credential-context strings Context name(s) in which to store credentials ($GPTSCRIPT_CREDENTIAL_CONTEXT)
2222
```
2323

2424
### SEE ALSO

docs/docs/04-command-line-reference/gptscript_credential_show.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ gptscript credential show <credential name> [flags]
1818
### Options inherited from parent commands
1919

2020
```
21-
--credential-context string Context name in which to store credentials ($GPTSCRIPT_CREDENTIAL_CONTEXT) (default "default")
21+
--credential-context strings Context name(s) in which to store credentials ($GPTSCRIPT_CREDENTIAL_CONTEXT)
2222
```
2323

2424
### SEE ALSO

docs/docs/04-command-line-reference/gptscript_eval.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ gptscript eval [flags]
3030
--color Use color in output (default true) ($GPTSCRIPT_COLOR)
3131
--config string Path to GPTScript config file ($GPTSCRIPT_CONFIG)
3232
--confirm Prompt before running potentially dangerous commands ($GPTSCRIPT_CONFIRM)
33-
--credential-context string Context name in which to store credentials ($GPTSCRIPT_CREDENTIAL_CONTEXT) (default "default")
33+
--credential-context strings Context name(s) in which to store credentials ($GPTSCRIPT_CREDENTIAL_CONTEXT)
3434
--credential-override strings Credentials to override (ex: --credential-override github.com/example/cred-tool:API_TOKEN=1234) ($GPTSCRIPT_CREDENTIAL_OVERRIDE)
3535
--debug Enable debug logging ($GPTSCRIPT_DEBUG)
3636
--debug-messages Enable logging of chat completion calls ($GPTSCRIPT_DEBUG_MESSAGES)

docs/docs/04-command-line-reference/gptscript_fmt.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ gptscript fmt [flags]
2424
--color Use color in output (default true) ($GPTSCRIPT_COLOR)
2525
--config string Path to GPTScript config file ($GPTSCRIPT_CONFIG)
2626
--confirm Prompt before running potentially dangerous commands ($GPTSCRIPT_CONFIRM)
27-
--credential-context string Context name in which to store credentials ($GPTSCRIPT_CREDENTIAL_CONTEXT) (default "default")
27+
--credential-context strings Context name(s) in which to store credentials ($GPTSCRIPT_CREDENTIAL_CONTEXT)
2828
--credential-override strings Credentials to override (ex: --credential-override github.com/example/cred-tool:API_TOKEN=1234) ($GPTSCRIPT_CREDENTIAL_OVERRIDE)
2929
--debug Enable debug logging ($GPTSCRIPT_DEBUG)
3030
--debug-messages Enable logging of chat completion calls ($GPTSCRIPT_DEBUG_MESSAGES)

docs/docs/04-command-line-reference/gptscript_getenv.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ gptscript getenv [flags] KEY [DEFAULT]
2323
--color Use color in output (default true) ($GPTSCRIPT_COLOR)
2424
--config string Path to GPTScript config file ($GPTSCRIPT_CONFIG)
2525
--confirm Prompt before running potentially dangerous commands ($GPTSCRIPT_CONFIRM)
26-
--credential-context string Context name in which to store credentials ($GPTSCRIPT_CREDENTIAL_CONTEXT) (default "default")
26+
--credential-context strings Context name(s) in which to store credentials ($GPTSCRIPT_CREDENTIAL_CONTEXT)
2727
--credential-override strings Credentials to override (ex: --credential-override github.com/example/cred-tool:API_TOKEN=1234) ($GPTSCRIPT_CREDENTIAL_OVERRIDE)
2828
--debug Enable debug logging ($GPTSCRIPT_DEBUG)
2929
--debug-messages Enable logging of chat completion calls ($GPTSCRIPT_DEBUG_MESSAGES)

docs/docs/04-command-line-reference/gptscript_parse.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ gptscript parse [flags]
2424
--color Use color in output (default true) ($GPTSCRIPT_COLOR)
2525
--config string Path to GPTScript config file ($GPTSCRIPT_CONFIG)
2626
--confirm Prompt before running potentially dangerous commands ($GPTSCRIPT_CONFIRM)
27-
--credential-context string Context name in which to store credentials ($GPTSCRIPT_CREDENTIAL_CONTEXT) (default "default")
27+
--credential-context strings Context name(s) in which to store credentials ($GPTSCRIPT_CREDENTIAL_CONTEXT)
2828
--credential-override strings Credentials to override (ex: --credential-override github.com/example/cred-tool:API_TOKEN=1234) ($GPTSCRIPT_CREDENTIAL_OVERRIDE)
2929
--debug Enable debug logging ($GPTSCRIPT_DEBUG)
3030
--debug-messages Enable logging of chat completion calls ($GPTSCRIPT_DEBUG_MESSAGES)

integration/cred_test.go

+54
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,57 @@ func TestCredentialExpirationEnv(t *testing.T) {
4545
}
4646
}
4747
}
48+
49+
// TestStackedCredentialContexts tests creating, using, listing, showing, and deleting credentials when there are multiple contexts.
50+
func TestStackedCredentialContexts(t *testing.T) {
51+
// First, test credential creation. We will create a credential called testcred in two different contexts called one and two.
52+
_, err := RunScript("scripts/cred_stacked.gpt", "--sub-tool", "testcred_one", "--credential-context", "one,two")
53+
require.NoError(t, err)
54+
55+
_, err = RunScript("scripts/cred_stacked.gpt", "--sub-tool", "testcred_two", "--credential-context", "two")
56+
require.NoError(t, err)
57+
58+
// Next, we try running the testcred_one tool. It should print the value of "testcred" in whichever context it finds the cred first.
59+
out, err := RunScript("scripts/cred_stacked.gpt", "--sub-tool", "testcred_one", "--credential-context", "one,two")
60+
require.NoError(t, err)
61+
require.Contains(t, out, "one")
62+
require.NotContains(t, out, "two")
63+
64+
out, err = RunScript("scripts/cred_stacked.gpt", "--sub-tool", "testcred_one", "--credential-context", "two,one")
65+
require.NoError(t, err)
66+
require.Contains(t, out, "two")
67+
require.NotContains(t, out, "one")
68+
69+
// Next, list credentials and specify both contexts. We should get the credential from the first specified context.
70+
out, err = GPTScriptExec("--credential-context", "one,two", "cred")
71+
require.NoError(t, err)
72+
require.Contains(t, out, "one")
73+
require.NotContains(t, out, "two")
74+
75+
out, err = GPTScriptExec("--credential-context", "two,one", "cred")
76+
require.NoError(t, err)
77+
require.Contains(t, out, "two")
78+
require.NotContains(t, out, "one")
79+
80+
// Next, try showing the credentials.
81+
out, err = GPTScriptExec("--credential-context", "one,two", "cred", "show", "testcred")
82+
require.NoError(t, err)
83+
require.Contains(t, out, "one")
84+
require.NotContains(t, out, "two")
85+
86+
out, err = GPTScriptExec("--credential-context", "two,one", "cred", "show", "testcred")
87+
require.NoError(t, err)
88+
require.Contains(t, out, "two")
89+
require.NotContains(t, out, "one")
90+
91+
// Make sure we get an error if we try to delete a credential with multiple contexts specified.
92+
_, err = GPTScriptExec("--credential-context", "one,two", "cred", "delete", "testcred")
93+
require.Error(t, err)
94+
95+
// Now actually delete the credentials.
96+
_, err = GPTScriptExec("--credential-context", "one", "cred", "delete", "testcred")
97+
require.NoError(t, err)
98+
99+
_, err = GPTScriptExec("--credential-context", "two", "cred", "delete", "testcred")
100+
require.NoError(t, err)
101+
}

integration/scripts/cred_stacked.gpt

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: testcred_one
2+
credential: cred_one as testcred
3+
4+
#!python3
5+
6+
import os
7+
8+
print(os.environ.get("VALUE"))
9+
10+
---
11+
name: testcred_two
12+
credential: cred_two as testcred
13+
14+
#!python3
15+
16+
import os
17+
18+
print(os.environ.get("VALUE"))
19+
20+
---
21+
name: cred_one
22+
23+
#!python3
24+
25+
import json
26+
27+
print(json.dumps({"env": {"VALUE": "one"}}))
28+
29+
---
30+
name: cred_two
31+
32+
#!python3
33+
34+
import json
35+
36+
print(json.dumps({"env": {"VALUE": "two"}}))

pkg/cli/credential.go

+10-12
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,10 @@ import (
99
"time"
1010

1111
cmd2 "github.com/gptscript-ai/cmd"
12-
"github.com/gptscript-ai/gptscript/pkg/cache"
1312
"github.com/gptscript-ai/gptscript/pkg/config"
1413
"github.com/gptscript-ai/gptscript/pkg/credentials"
14+
"github.com/gptscript-ai/gptscript/pkg/gptscript"
1515
"github.com/gptscript-ai/gptscript/pkg/repos/runtimes"
16-
"github.com/gptscript-ai/gptscript/pkg/runner"
1716
"github.com/spf13/cobra"
1817
)
1918

@@ -43,27 +42,26 @@ func (c *Credential) Run(cmd *cobra.Command, _ []string) error {
4342
return fmt.Errorf("failed to read CLI config: %w", err)
4443
}
4544

46-
ctx := c.root.CredentialContext
47-
if c.AllContexts {
48-
ctx = credentials.AllCredentialContexts
49-
}
50-
5145
opts, err := c.root.NewGPTScriptOpts()
5246
if err != nil {
5347
return err
5448
}
55-
opts.Cache = cache.Complete(opts.Cache)
56-
opts.Runner = runner.Complete(opts.Runner)
49+
opts = gptscript.Complete(opts)
5750
if opts.Runner.RuntimeManager == nil {
5851
opts.Runner.RuntimeManager = runtimes.Default(opts.Cache.CacheDir)
5952
}
6053

54+
ctxs := opts.CredentialContexts
55+
if c.AllContexts {
56+
ctxs = []string{credentials.AllCredentialContexts}
57+
}
58+
6159
if err = opts.Runner.RuntimeManager.SetUpCredentialHelpers(cmd.Context(), cfg); err != nil {
6260
return err
6361
}
6462

6563
// Initialize the credential store and get all the credentials.
66-
store, err := credentials.NewStore(cfg, opts.Runner.RuntimeManager, ctx, opts.Cache.CacheDir)
64+
store, err := credentials.NewStore(cfg, opts.Runner.RuntimeManager, ctxs, opts.Cache.CacheDir)
6765
if err != nil {
6866
return fmt.Errorf("failed to get credentials store: %w", err)
6967
}
@@ -77,7 +75,7 @@ func (c *Credential) Run(cmd *cobra.Command, _ []string) error {
7775
defer w.Flush()
7876

7977
// Sort credentials and print column names, depending on the options.
80-
if c.AllContexts {
78+
if c.AllContexts || len(c.root.CredentialContext) > 1 {
8179
// Sort credentials by context
8280
sort.Slice(creds, func(i, j int) bool {
8381
if creds[i].Context == creds[j].Context {
@@ -114,7 +112,7 @@ func (c *Credential) Run(cmd *cobra.Command, _ []string) error {
114112
}
115113

116114
var fields []any
117-
if c.AllContexts {
115+
if c.AllContexts || len(c.root.CredentialContext) > 1 {
118116
fields = []any{cred.Context, cred.ToolName, expires}
119117
} else {
120118
fields = []any{cred.ToolName, expires}

pkg/cli/credential_delete.go

+3-5
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,10 @@ package cli
33
import (
44
"fmt"
55

6-
"github.com/gptscript-ai/gptscript/pkg/cache"
76
"github.com/gptscript-ai/gptscript/pkg/config"
87
"github.com/gptscript-ai/gptscript/pkg/credentials"
8+
"github.com/gptscript-ai/gptscript/pkg/gptscript"
99
"github.com/gptscript-ai/gptscript/pkg/repos/runtimes"
10-
"github.com/gptscript-ai/gptscript/pkg/runner"
1110
"github.com/spf13/cobra"
1211
)
1312

@@ -34,8 +33,7 @@ func (c *Delete) Run(cmd *cobra.Command, args []string) error {
3433
return fmt.Errorf("failed to read CLI config: %w", err)
3534
}
3635

37-
opts.Cache = cache.Complete(opts.Cache)
38-
opts.Runner = runner.Complete(opts.Runner)
36+
opts = gptscript.Complete(opts)
3937
if opts.Runner.RuntimeManager == nil {
4038
opts.Runner.RuntimeManager = runtimes.Default(opts.Cache.CacheDir)
4139
}
@@ -44,7 +42,7 @@ func (c *Delete) Run(cmd *cobra.Command, args []string) error {
4442
return err
4543
}
4644

47-
store, err := credentials.NewStore(cfg, opts.Runner.RuntimeManager, c.root.CredentialContext, opts.Cache.CacheDir)
45+
store, err := credentials.NewStore(cfg, opts.Runner.RuntimeManager, opts.CredentialContexts, opts.Cache.CacheDir)
4846
if err != nil {
4947
return fmt.Errorf("failed to get credentials store: %w", err)
5048
}

pkg/cli/credential_show.go

+3-5
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,10 @@ import (
55
"os"
66
"text/tabwriter"
77

8-
"github.com/gptscript-ai/gptscript/pkg/cache"
98
"github.com/gptscript-ai/gptscript/pkg/config"
109
"github.com/gptscript-ai/gptscript/pkg/credentials"
10+
"github.com/gptscript-ai/gptscript/pkg/gptscript"
1111
"github.com/gptscript-ai/gptscript/pkg/repos/runtimes"
12-
"github.com/gptscript-ai/gptscript/pkg/runner"
1312
"github.com/spf13/cobra"
1413
)
1514

@@ -36,8 +35,7 @@ func (c *Show) Run(cmd *cobra.Command, args []string) error {
3635
return fmt.Errorf("failed to read CLI config: %w", err)
3736
}
3837

39-
opts.Cache = cache.Complete(opts.Cache)
40-
opts.Runner = runner.Complete(opts.Runner)
38+
opts = gptscript.Complete(opts)
4139
if opts.Runner.RuntimeManager == nil {
4240
opts.Runner.RuntimeManager = runtimes.Default(opts.Cache.CacheDir)
4341
}
@@ -46,7 +44,7 @@ func (c *Show) Run(cmd *cobra.Command, args []string) error {
4644
return err
4745
}
4846

49-
store, err := credentials.NewStore(cfg, opts.Runner.RuntimeManager, c.root.CredentialContext, opts.Cache.CacheDir)
47+
store, err := credentials.NewStore(cfg, opts.Runner.RuntimeManager, opts.CredentialContexts, opts.Cache.CacheDir)
5048
if err != nil {
5149
return fmt.Errorf("failed to get credentials store: %w", err)
5250
}

pkg/cli/gptscript.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ type GPTScript struct {
6464
Chdir string `usage:"Change current working directory" short:"C"`
6565
Daemon bool `usage:"Run tool as a daemon" local:"true" hidden:"true"`
6666
Ports string `usage:"The port range to use for ephemeral daemon ports (ex: 11000-12000)" hidden:"true"`
67-
CredentialContext string `usage:"Context name in which to store credentials" default:"default"`
67+
CredentialContext []string `usage:"Context name(s) in which to store credentials"`
6868
CredentialOverride []string `usage:"Credentials to override (ex: --credential-override github.com/example/cred-tool:API_TOKEN=1234)"`
6969
ChatState string `usage:"The chat state to continue, or null to start a new chat and return the state" local:"true"`
7070
ForceChat bool `usage:"Force an interactive chat session if even the top level tool is not a chat tool" local:"true"`
@@ -142,7 +142,7 @@ func (r *GPTScript) NewGPTScriptOpts() (gptscript.Options, error) {
142142
},
143143
Quiet: r.Quiet,
144144
Env: os.Environ(),
145-
CredentialContext: r.CredentialContext,
145+
CredentialContexts: r.CredentialContext,
146146
Workspace: r.Workspace,
147147
DisablePromptServer: r.UI,
148148
DefaultModelProvider: r.DefaultModelProvider,

0 commit comments

Comments
 (0)