Skip to content

Commit f4cb837

Browse files
committed
enhance: add support for args and aliases to credential tools
Signed-off-by: Grant Linville <[email protected]>
1 parent 8f344d9 commit f4cb837

File tree

5 files changed

+360
-31
lines changed

5 files changed

+360
-31
lines changed

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

+50-9
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ directly from user input) and conveniently set them in the environment before ru
88
A credential provider tool looks just like any other GPTScript, with the following caveats:
99
- It cannot call the LLM and must run a command.
1010
- 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.
1211

1312
Here is a simple example of a credential provider tool that uses the builtin `sys.prompt` to ask the user for some input:
1413

@@ -50,6 +49,30 @@ credentials: credential-tool-1.gpt, credential-tool-2.gpt
5049
(tool stuff here)
5150
```
5251

52+
## Credential Tool Arguments
53+
54+
A credential tool may define arguments. Here is an example:
55+
56+
```yaml
57+
name: my-credential-tool
58+
args: env: the environment variable to set
59+
args: val: the value to set it to
60+
61+
#!/usr/bin/env bash
62+
63+
echo "{\"env\":{\"$ENV\":\"$VAL\"}}"
64+
```
65+
66+
When you reference this credential tool in another file, you can use syntax like this to set both arguments:
67+
68+
```yaml
69+
credential: my-credential-tool.gpt with MY_ENV_VAR as env and "my value" as val
70+
71+
(tool stuff here)
72+
```
73+
74+
In this example, the tool's output would be `{"env":{"MY_ENV_VAR":"my value"}}`
75+
5376
## Storing Credentials
5477

5578
By default, credentials are automatically stored in a config file at `$XDG_CONFIG_HOME/gptscript/config.json`.
@@ -67,16 +90,30 @@ is called `gptscript-credential-wincred.exe`.)
6790
There will likely be support added for other credential stores in the future.
6891

6992
:::note
70-
Credentials received from credential provider tools that are not on GitHub (such as a local file) will not be stored
71-
in the credentials store.
93+
Credentials received from credential provider tools that are not on GitHub (such as a local file) and do not have an alias
94+
will not be stored in the credentials store.
7295
:::
7396

97+
## Credential Aliases
98+
99+
When you reference a credential tool in your script, you can give it an alias using the `as` keyword like this:
100+
101+
```yaml
102+
credentials: my-credential-tool.gpt as myAlias
103+
104+
(tool stuff here)
105+
```
106+
107+
This will store the resulting credential with the name `myAlias`.
108+
This is useful when you want to reference the same credential tool in scripts that need to handle different credentials,
109+
or when you want to store credentials that were provided by a tool that is not on GitHub.
110+
74111
## Credential Contexts
75112

76-
Each stored credential is uniquely identified by the name of its provider tool and the name of its context. A credential
77-
context is basically a namespace for credentials. If you have multiple credentials from the same provider tool, you can
78-
switch between them by defining them in different credential contexts. The default context is called `default`, and this
79-
is used if none is specified.
113+
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.
114+
A credential context is basically a namespace for credentials. If you have multiple credentials from the same provider tool,
115+
you can switch between them by defining them in different credential contexts. The default context is called `default`,
116+
and this is used if none is specified.
80117

81118
You can set the credential context to use with the `--credential-context` flag when running GPTScript. For
82119
example:
@@ -97,13 +134,17 @@ credentials in all contexts with `--all-contexts`.
97134
You can delete a credential by running the following command:
98135

99136
```bash
100-
gptscript credential delete --credential-context <credential context> <credential tool name>
137+
gptscript credential delete --credential-context <credential context> <credential name>
101138
```
102139

103140
The `--show-env-vars` argument will also display the names of the environment variables that are set by the credential.
104141
This is useful when working with credential overrides.
105142

106-
## Credential Overrides
143+
## Credential Overrides (Advanced)
144+
145+
:::note
146+
The syntax for this will change at some point in the future.
147+
:::
107148

108149
You can bypass credential tools and stored credentials by setting the `--credential-override` argument (or the
109150
`GPTSCRIPT_CREDENTIAL_OVERRIDE` environment variable) when running GPTScript. To set up a credential override, you

pkg/parser/parser.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ func isParam(line string, tool *types.Tool) (_ bool, err error) {
138138
return false, err
139139
}
140140
case "credentials", "creds", "credential", "cred":
141-
tool.Parameters.Credentials = append(tool.Parameters.Credentials, csv(strings.ToLower(value))...)
141+
tool.Parameters.Credentials = append(tool.Parameters.Credentials, csv(value)...)
142142
default:
143143
return false, nil
144144
}

pkg/runner/runner.go

+38-15
Original file line numberDiff line numberDiff line change
@@ -816,17 +816,27 @@ func (r *Runner) handleCredentials(callCtx engine.Context, monitor Monitor, env
816816
continue
817817
}
818818

819+
toolName, credentialAlias, args, err := types.ParseCredentialArgs(credToolName, callCtx.Input)
820+
if err != nil {
821+
return nil, fmt.Errorf("failed to parse credential tool %q: %w", credToolName, err)
822+
}
823+
819824
var (
820825
cred *credentials.Credential
821826
exists bool
822-
err error
823827
)
824828

825-
// Only try to look up the cred if the tool is on GitHub.
826-
if isGitHubTool(credToolName) {
827-
cred, exists, err = store.Get(credToolName)
829+
// Only try to look up the cred if the tool is on GitHub or has an alias.
830+
// If it is a GitHub tool and has an alias, the alias overrides the tool name, so we use it as the credential name.
831+
if isGitHubTool(toolName) && credentialAlias == "" {
832+
cred, exists, err = store.Get(toolName)
828833
if err != nil {
829-
return nil, fmt.Errorf("failed to get credentials for tool %s: %w", credToolName, err)
834+
return nil, fmt.Errorf("failed to get credentials for tool %s: %w", toolName, err)
835+
}
836+
} else if credentialAlias != "" {
837+
cred, exists, err = store.Get(credentialAlias)
838+
if err != nil {
839+
return nil, fmt.Errorf("failed to get credentials for tool %s: %w", credentialAlias, err)
830840
}
831841
}
832842

@@ -838,12 +848,21 @@ func (r *Runner) handleCredentials(callCtx engine.Context, monitor Monitor, env
838848
return nil, fmt.Errorf("failed to find ID for tool %s", credToolName)
839849
}
840850

841-
subCtx, err := callCtx.SubCall(callCtx.Ctx, "", credToolRefs[0].ToolID, "", engine.CredentialToolCategory) // leaving callID as "" will cause it to be set by the engine
851+
input := ""
852+
if args != nil {
853+
inputBytes, err := json.Marshal(args)
854+
if err != nil {
855+
return nil, fmt.Errorf("failed to marshal args for tool %s: %w", credToolName, err)
856+
}
857+
input = string(inputBytes)
858+
}
859+
860+
subCtx, err := callCtx.SubCall(callCtx.Ctx, input, credToolRefs[0].ToolID, "", engine.CredentialToolCategory) // leaving callID as "" will cause it to be set by the engine
842861
if err != nil {
843862
return nil, fmt.Errorf("failed to create subcall context for tool %s: %w", credToolName, err)
844863
}
845864

846-
res, err := r.call(subCtx, monitor, env, "")
865+
res, err := r.call(subCtx, monitor, env, input)
847866
if err != nil {
848867
return nil, fmt.Errorf("failed to run credential tool %s: %w", credToolName, err)
849868
}
@@ -860,9 +879,13 @@ func (r *Runner) handleCredentials(callCtx engine.Context, monitor Monitor, env
860879
}
861880

862881
cred = &credentials.Credential{
863-
ToolName: credToolName,
864-
Type: credentials.CredentialTypeTool,
865-
Env: envMap.Env,
882+
Type: credentials.CredentialTypeTool,
883+
Env: envMap.Env,
884+
}
885+
if credentialAlias != "" {
886+
cred.ToolName = credentialAlias
887+
} else {
888+
cred.ToolName = toolName
866889
}
867890

868891
isEmpty := true
@@ -873,15 +896,15 @@ func (r *Runner) handleCredentials(callCtx engine.Context, monitor Monitor, env
873896
}
874897
}
875898

876-
// Only store the credential if the tool is on GitHub, and the credential is non-empty.
877-
if isGitHubTool(credToolName) && callCtx.Program.ToolSet[credToolRefs[0].ToolID].Source.Repo != nil {
899+
// Only store the credential if the tool is on GitHub or has an alias, and the credential is non-empty.
900+
if (isGitHubTool(toolName) && callCtx.Program.ToolSet[credToolRefs[0].ToolID].Source.Repo != nil) || credentialAlias != "" {
878901
if isEmpty {
879-
log.Warnf("Not saving empty credential for tool %s", credToolName)
902+
log.Warnf("Not saving empty credential for tool %s", toolName)
880903
} else if err := store.Add(*cred); err != nil {
881-
return nil, fmt.Errorf("failed to add credential for tool %s: %w", credToolName, err)
904+
return nil, fmt.Errorf("failed to add credential for tool %s: %w", toolName, err)
882905
}
883906
} else {
884-
log.Warnf("Not saving credential for local tool %s - credentials will only be saved for tools from GitHub.", credToolName)
907+
log.Warnf("Not saving credential for tool %s - credentials will only be saved for tools from GitHub, or tools that use aliases.", toolName)
885908
}
886909
}
887910

pkg/types/credential_test.go

+153
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
package types
2+
3+
import "testing"
4+
5+
func TestParseCredentialArgs(t *testing.T) {
6+
tests := []struct {
7+
name string
8+
toolName string
9+
input string
10+
expectedName string
11+
expectedAlias string
12+
expectedArgs map[string]string
13+
wantErr bool
14+
}{
15+
{
16+
name: "empty",
17+
toolName: "",
18+
expectedName: "",
19+
expectedAlias: "",
20+
},
21+
{
22+
name: "tool name only",
23+
toolName: "myCredentialTool",
24+
expectedName: "myCredentialTool",
25+
expectedAlias: "",
26+
},
27+
{
28+
name: "tool name and alias",
29+
toolName: "myCredentialTool as myAlias",
30+
expectedName: "myCredentialTool",
31+
expectedAlias: "myAlias",
32+
},
33+
{
34+
name: "tool name with one arg",
35+
toolName: "myCredentialTool with value1 as arg1",
36+
expectedName: "myCredentialTool",
37+
expectedAlias: "",
38+
expectedArgs: map[string]string{
39+
"arg1": "value1",
40+
},
41+
},
42+
{
43+
name: "tool name with two args",
44+
toolName: "myCredentialTool with value1 as arg1 and value2 as arg2",
45+
expectedName: "myCredentialTool",
46+
expectedAlias: "",
47+
expectedArgs: map[string]string{
48+
"arg1": "value1",
49+
"arg2": "value2",
50+
},
51+
},
52+
{
53+
name: "tool name with alias and one arg",
54+
toolName: "myCredentialTool as myAlias with value1 as arg1",
55+
expectedName: "myCredentialTool",
56+
expectedAlias: "myAlias",
57+
expectedArgs: map[string]string{
58+
"arg1": "value1",
59+
},
60+
},
61+
{
62+
name: "tool name with alias and two args",
63+
toolName: "myCredentialTool as myAlias with value1 as arg1 and value2 as arg2",
64+
expectedName: "myCredentialTool",
65+
expectedAlias: "myAlias",
66+
expectedArgs: map[string]string{
67+
"arg1": "value1",
68+
"arg2": "value2",
69+
},
70+
},
71+
{
72+
name: "tool name with quoted args",
73+
toolName: `myCredentialTool with "value one" as arg1 and "value two" as arg2`,
74+
expectedName: "myCredentialTool",
75+
expectedAlias: "",
76+
expectedArgs: map[string]string{
77+
"arg1": "value one",
78+
"arg2": "value two",
79+
},
80+
},
81+
{
82+
name: "tool name with arg references",
83+
toolName: `myCredentialTool with ${var1} as arg1 and ${var2} as arg2`,
84+
input: `{"var1": "value1", "var2": "value2"}`,
85+
expectedName: "myCredentialTool",
86+
expectedAlias: "",
87+
expectedArgs: map[string]string{
88+
"arg1": "value1",
89+
"arg2": "value2",
90+
},
91+
},
92+
{
93+
name: "tool name with alias but no 'as' (invalid)",
94+
toolName: "myCredentialTool myAlias",
95+
wantErr: true,
96+
},
97+
{
98+
name: "tool name with 'as' but no alias (invalid)",
99+
toolName: "myCredentialTool as",
100+
wantErr: true,
101+
},
102+
{
103+
name: "tool with 'with' but no args (invalid)",
104+
toolName: "myCredentialTool with",
105+
wantErr: true,
106+
},
107+
{
108+
name: "tool with args but no 'with' (invalid)",
109+
toolName: "myCredentialTool value1 as arg1",
110+
wantErr: true,
111+
},
112+
{
113+
name: "tool with trailing 'and' (invalid)",
114+
toolName: "myCredentialTool with value1 as arg1 and",
115+
wantErr: true,
116+
},
117+
{
118+
name: "tool with quoted arg but the quote is unterminated (invalid)",
119+
toolName: `myCredentialTool with "value one" as arg1 and "value two as arg2`,
120+
wantErr: true,
121+
},
122+
{
123+
name: "invalid input",
124+
toolName: "myCredentialTool",
125+
input: `{"asdf":"asdf"`,
126+
wantErr: true,
127+
},
128+
}
129+
130+
for _, tt := range tests {
131+
t.Run(tt.name, func(t *testing.T) {
132+
originalName, alias, args, err := ParseCredentialArgs(tt.toolName, tt.input)
133+
if (err != nil) != tt.wantErr {
134+
t.Errorf("ParseCredentialArgs() error = %v, wantErr %v", err, tt.wantErr)
135+
return
136+
}
137+
if originalName != tt.expectedName {
138+
t.Errorf("ParseCredentialArgs() originalName = %v, expectedName %v", originalName, tt.expectedName)
139+
}
140+
if alias != tt.expectedAlias {
141+
t.Errorf("ParseCredentialArgs() alias = %v, expectedAlias %v", alias, tt.expectedAlias)
142+
}
143+
if len(args) != len(tt.expectedArgs) {
144+
t.Errorf("ParseCredentialArgs() args = %v, expectedArgs %v", args, tt.expectedArgs)
145+
}
146+
for k, v := range tt.expectedArgs {
147+
if args[k] != v {
148+
t.Errorf("ParseCredentialArgs() args[%s] = %v, expectedArgs[%s] %v", k, args[k], k, v)
149+
}
150+
}
151+
})
152+
}
153+
}

0 commit comments

Comments
 (0)