Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add stacked credential contexts #849

Merged
merged 9 commits into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions docs/docs/03-tools/04-credential-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,3 +222,55 @@ import os

print("myCred expires at " + os.getenv("GPTSCRIPT_CREDENTIAL_EXPIRATION", ""))
```

## Stacked Credential Contexts (Advanced)

When setting the `--credential-context` argument in GPTScript, you can specify multiple contexts separated by commas.
We refer to this as "stacked credential contexts", or just stacked contexts for short. This allows you to specify an order
of priority for credential contexts. This is best explained by example.

### Example: stacked contexts when running a script that uses a credential

Let's say you have two contexts, `one` and `two`, and you specify them like this:

```bash
gptscript --credential-context one,two my-script.gpt
```

```
Credential: my-credential-tool.gpt as myCred

<tool stuff here>
```

When GPTScript runs, it will first look for a credential called `myCred` in the `one` context.
If it doesn't find it there, it will look for it in the `two` context. If it also doesn't find it there,
it will run the `my-credential-tool.gpt` tool to get the credential. It will then store the new credential into the `one`
context, since that has the highest priority.

### Example: stacked contexts when listing credentials

```bash
gptscript --credential-context one,two credentials
```

When you list credentials like this, GPTScript will print out the information for all credentials in contexts one and two,
with one exception. If there is a credential name that exists in both contexts, GPTScript will only print the information
for the credential in the context with the highest priority, which in this case is `one`.

(To see all credentials in all contexts, you can still use the `--all-contexts` flag, and it will show all credentials,
regardless of whether the same name appears in another context.)

### Example: stacked contexts when showing credentials

```bash
gptscript --credential-context one,two credential show myCred
```

When you show a credential like this, GPTScript will first look for `myCred` in the `one` context. If it doesn't find it
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.

:::note
You cannot specify stacked contexts when doing `gptscript credential delete`. GPTScript will return an error if
more than one context is specified for this command.
:::
2 changes: 1 addition & 1 deletion docs/docs/04-command-line-reference/gptscript.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ gptscript [flags] PROGRAM_FILE [INPUT...]
--color Use color in output (default true) ($GPTSCRIPT_COLOR)
--config string Path to GPTScript config file ($GPTSCRIPT_CONFIG)
--confirm Prompt before running potentially dangerous commands ($GPTSCRIPT_CONFIRM)
--credential-context string Context name in which to store credentials ($GPTSCRIPT_CREDENTIAL_CONTEXT) (default "default")
--credential-context strings Context name(s) in which to store credentials ($GPTSCRIPT_CREDENTIAL_CONTEXT)
--credential-override strings Credentials to override (ex: --credential-override github.com/example/cred-tool:API_TOKEN=1234) ($GPTSCRIPT_CREDENTIAL_OVERRIDE)
--debug Enable debug logging ($GPTSCRIPT_DEBUG)
--debug-messages Enable logging of chat completion calls ($GPTSCRIPT_DEBUG_MESSAGES)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ gptscript credential [flags]
### Options inherited from parent commands

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

### SEE ALSO
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ gptscript credential delete <credential name> [flags]
### Options inherited from parent commands

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

### SEE ALSO
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ gptscript credential show <credential name> [flags]
### Options inherited from parent commands

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

### SEE ALSO
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/04-command-line-reference/gptscript_eval.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ gptscript eval [flags]
--color Use color in output (default true) ($GPTSCRIPT_COLOR)
--config string Path to GPTScript config file ($GPTSCRIPT_CONFIG)
--confirm Prompt before running potentially dangerous commands ($GPTSCRIPT_CONFIRM)
--credential-context string Context name in which to store credentials ($GPTSCRIPT_CREDENTIAL_CONTEXT) (default "default")
--credential-context strings Context name(s) in which to store credentials ($GPTSCRIPT_CREDENTIAL_CONTEXT)
--credential-override strings Credentials to override (ex: --credential-override github.com/example/cred-tool:API_TOKEN=1234) ($GPTSCRIPT_CREDENTIAL_OVERRIDE)
--debug Enable debug logging ($GPTSCRIPT_DEBUG)
--debug-messages Enable logging of chat completion calls ($GPTSCRIPT_DEBUG_MESSAGES)
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/04-command-line-reference/gptscript_fmt.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ gptscript fmt [flags]
--color Use color in output (default true) ($GPTSCRIPT_COLOR)
--config string Path to GPTScript config file ($GPTSCRIPT_CONFIG)
--confirm Prompt before running potentially dangerous commands ($GPTSCRIPT_CONFIRM)
--credential-context string Context name in which to store credentials ($GPTSCRIPT_CREDENTIAL_CONTEXT) (default "default")
--credential-context strings Context name(s) in which to store credentials ($GPTSCRIPT_CREDENTIAL_CONTEXT)
--credential-override strings Credentials to override (ex: --credential-override github.com/example/cred-tool:API_TOKEN=1234) ($GPTSCRIPT_CREDENTIAL_OVERRIDE)
--debug Enable debug logging ($GPTSCRIPT_DEBUG)
--debug-messages Enable logging of chat completion calls ($GPTSCRIPT_DEBUG_MESSAGES)
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/04-command-line-reference/gptscript_getenv.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ gptscript getenv [flags] KEY [DEFAULT]
--color Use color in output (default true) ($GPTSCRIPT_COLOR)
--config string Path to GPTScript config file ($GPTSCRIPT_CONFIG)
--confirm Prompt before running potentially dangerous commands ($GPTSCRIPT_CONFIRM)
--credential-context string Context name in which to store credentials ($GPTSCRIPT_CREDENTIAL_CONTEXT) (default "default")
--credential-context strings Context name(s) in which to store credentials ($GPTSCRIPT_CREDENTIAL_CONTEXT)
--credential-override strings Credentials to override (ex: --credential-override github.com/example/cred-tool:API_TOKEN=1234) ($GPTSCRIPT_CREDENTIAL_OVERRIDE)
--debug Enable debug logging ($GPTSCRIPT_DEBUG)
--debug-messages Enable logging of chat completion calls ($GPTSCRIPT_DEBUG_MESSAGES)
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/04-command-line-reference/gptscript_parse.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ gptscript parse [flags]
--color Use color in output (default true) ($GPTSCRIPT_COLOR)
--config string Path to GPTScript config file ($GPTSCRIPT_CONFIG)
--confirm Prompt before running potentially dangerous commands ($GPTSCRIPT_CONFIRM)
--credential-context string Context name in which to store credentials ($GPTSCRIPT_CREDENTIAL_CONTEXT) (default "default")
--credential-context strings Context name(s) in which to store credentials ($GPTSCRIPT_CREDENTIAL_CONTEXT)
--credential-override strings Credentials to override (ex: --credential-override github.com/example/cred-tool:API_TOKEN=1234) ($GPTSCRIPT_CREDENTIAL_OVERRIDE)
--debug Enable debug logging ($GPTSCRIPT_DEBUG)
--debug-messages Enable logging of chat completion calls ($GPTSCRIPT_DEBUG_MESSAGES)
Expand Down
54 changes: 54 additions & 0 deletions integration/cred_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,57 @@ func TestCredentialExpirationEnv(t *testing.T) {
}
}
}

// TestStackedCredentialContexts tests creating, using, listing, showing, and deleting credentials when there are multiple contexts.
func TestStackedCredentialContexts(t *testing.T) {
// First, test credential creation. We will create a credential called testcred in two different contexts called one and two.
_, err := RunScript("scripts/cred_stacked.gpt", "--sub-tool", "testcred_one", "--credential-context", "one,two")
require.NoError(t, err)

_, err = RunScript("scripts/cred_stacked.gpt", "--sub-tool", "testcred_two", "--credential-context", "two")
require.NoError(t, err)

// Next, we try running the testcred_one tool. It should print the value of "testcred" in whichever context it finds the cred first.
out, err := RunScript("scripts/cred_stacked.gpt", "--sub-tool", "testcred_one", "--credential-context", "one,two")
require.NoError(t, err)
require.Contains(t, out, "one")
require.NotContains(t, out, "two")

out, err = RunScript("scripts/cred_stacked.gpt", "--sub-tool", "testcred_one", "--credential-context", "two,one")
require.NoError(t, err)
require.Contains(t, out, "two")
require.NotContains(t, out, "one")

// Next, list credentials and specify both contexts. We should get the credential from the first specified context.
out, err = GPTScriptExec("--credential-context", "one,two", "cred")
require.NoError(t, err)
require.Contains(t, out, "one")
require.NotContains(t, out, "two")

out, err = GPTScriptExec("--credential-context", "two,one", "cred")
require.NoError(t, err)
require.Contains(t, out, "two")
require.NotContains(t, out, "one")

// Next, try showing the credentials.
out, err = GPTScriptExec("--credential-context", "one,two", "cred", "show", "testcred")
require.NoError(t, err)
require.Contains(t, out, "one")
require.NotContains(t, out, "two")

out, err = GPTScriptExec("--credential-context", "two,one", "cred", "show", "testcred")
require.NoError(t, err)
require.Contains(t, out, "two")
require.NotContains(t, out, "one")

// Make sure we get an error if we try to delete a credential with multiple contexts specified.
_, err = GPTScriptExec("--credential-context", "one,two", "cred", "delete", "testcred")
require.Error(t, err)

// Now actually delete the credentials.
_, err = GPTScriptExec("--credential-context", "one", "cred", "delete", "testcred")
require.NoError(t, err)

_, err = GPTScriptExec("--credential-context", "two", "cred", "delete", "testcred")
require.NoError(t, err)
}
36 changes: 36 additions & 0 deletions integration/scripts/cred_stacked.gpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: testcred_one
credential: cred_one as testcred

#!python3

import os

print(os.environ.get("VALUE"))

---
name: testcred_two
credential: cred_two as testcred

#!python3

import os

print(os.environ.get("VALUE"))

---
name: cred_one

#!python3

import json

print(json.dumps({"env": {"VALUE": "one"}}))

---
name: cred_two

#!python3

import json

print(json.dumps({"env": {"VALUE": "two"}}))
12 changes: 7 additions & 5 deletions pkg/cli/credential.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,11 @@ func (c *Credential) Run(cmd *cobra.Command, _ []string) error {
return fmt.Errorf("failed to read CLI config: %w", err)
}

ctx := c.root.CredentialContext
ctxs := c.root.CredentialContext
if c.AllContexts {
ctx = credentials.AllCredentialContexts
ctxs = []string{credentials.AllCredentialContexts}
} else if len(ctxs) == 0 {
ctxs = []string{credentials.DefaultCredentialContext}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a few places where you have added this logic. Can this logic be moved to the Complete function for the gptscript options instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it already is in the Complete function in the gptscript/gptscript.go file. But without it in places like this, the c.root.CredentialContext still ends up being empty when the user doesn't specify anything. Not sure why that's the case. Should I be manually calling the Complete function here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm surprised this isn't working as I expect, then.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took a longer look at this: instead of completing the entire opts (created on line 53), we are completing individual pieces of it (lines 55 and 56).

It would be better to complete everything instead of the individual pieces, but I won't hold up merging this based on that.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sweet, that worked. Thanks. I should have spent more time looking at this myself, lol

}

opts, err := c.root.NewGPTScriptOpts()
Expand All @@ -63,7 +65,7 @@ func (c *Credential) Run(cmd *cobra.Command, _ []string) error {
}

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

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

var fields []any
if c.AllContexts {
if c.AllContexts || len(c.root.CredentialContext) > 1 {
fields = []any{cred.Context, cred.ToolName, expires}
} else {
fields = []any{cred.ToolName, expires}
Expand Down
7 changes: 6 additions & 1 deletion pkg/cli/credential_delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,16 @@ func (c *Delete) Run(cmd *cobra.Command, args []string) error {
opts.Runner.RuntimeManager = runtimes.Default(opts.Cache.CacheDir)
}

credCtx := c.root.CredentialContext
if len(credCtx) == 0 {
credCtx = []string{credentials.DefaultCredentialContext}
}

if err = opts.Runner.RuntimeManager.SetUpCredentialHelpers(cmd.Context(), cfg); err != nil {
return err
}

store, err := credentials.NewStore(cfg, opts.Runner.RuntimeManager, c.root.CredentialContext, opts.Cache.CacheDir)
store, err := credentials.NewStore(cfg, opts.Runner.RuntimeManager, credCtx, opts.Cache.CacheDir)
if err != nil {
return fmt.Errorf("failed to get credentials store: %w", err)
}
Expand Down
7 changes: 6 additions & 1 deletion pkg/cli/credential_show.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,16 @@ func (c *Show) Run(cmd *cobra.Command, args []string) error {
opts.Runner.RuntimeManager = runtimes.Default(opts.Cache.CacheDir)
}

credCtx := c.root.CredentialContext
if len(credCtx) == 0 {
credCtx = []string{credentials.DefaultCredentialContext}
}

if err = opts.Runner.RuntimeManager.SetUpCredentialHelpers(cmd.Context(), cfg); err != nil {
return err
}

store, err := credentials.NewStore(cfg, opts.Runner.RuntimeManager, c.root.CredentialContext, opts.Cache.CacheDir)
store, err := credentials.NewStore(cfg, opts.Runner.RuntimeManager, credCtx, opts.Cache.CacheDir)
if err != nil {
return fmt.Errorf("failed to get credentials store: %w", err)
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/cli/gptscript.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ type GPTScript struct {
Chdir string `usage:"Change current working directory" short:"C"`
Daemon bool `usage:"Run tool as a daemon" local:"true" hidden:"true"`
Ports string `usage:"The port range to use for ephemeral daemon ports (ex: 11000-12000)" hidden:"true"`
CredentialContext string `usage:"Context name in which to store credentials" default:"default"`
Copy link
Member Author

@g-linville g-linville Sep 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our cmd library doesn't support default values for slices, which is why I removed the default tag here.

CredentialContext []string `usage:"Context name(s) in which to store credentials"`
CredentialOverride []string `usage:"Credentials to override (ex: --credential-override github.com/example/cred-tool:API_TOKEN=1234)"`
ChatState string `usage:"The chat state to continue, or null to start a new chat and return the state" local:"true"`
ForceChat bool `usage:"Force an interactive chat session if even the top level tool is not a chat tool" local:"true"`
Expand Down Expand Up @@ -142,7 +142,7 @@ func (r *GPTScript) NewGPTScriptOpts() (gptscript.Options, error) {
},
Quiet: r.Quiet,
Env: os.Environ(),
CredentialContext: r.CredentialContext,
CredentialContexts: r.CredentialContext,
Workspace: r.Workspace,
DisablePromptServer: r.UI,
DefaultModelProvider: r.DefaultModelProvider,
Expand Down
Loading