Skip to content

Commit b77cd13

Browse files
authored
enhance: credentials: add GPTSCRIPT_CREDENTIAL_EXPIRATION (#709)
Signed-off-by: Grant Linville <[email protected]>
1 parent 160a733 commit b77cd13

File tree

7 files changed

+97
-3
lines changed

7 files changed

+97
-3
lines changed

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,3 +204,21 @@ that environment variable, and if it is set, get the refresh token from the exis
204204
typically without user interaction.
205205

206206
For an example of a tool that uses the refresh feature, see the [Gateway OAuth2 tool](https://github.com/gptscript-ai/gateway-oauth2).
207+
208+
### GPTSCRIPT_CREDENTIAL_EXPIRATION environment variable
209+
210+
When a tool references a credential tool, GPTScript will add the environment variables from the credential to the tool's
211+
environment before executing the tool. If at least one of the credentials has an `expiresAt` field, GPTScript will also
212+
set the environment variable `GPTSCRIPT_CREDENTIAL_EXPIRATION`, which contains the nearest expiration time out of all
213+
credentials referenced by the tool, in RFC 3339 format. That way, it can be referenced in the tool body if needed.
214+
Here is an example:
215+
216+
```
217+
Credential: my-credential-tool.gpt as myCred
218+
219+
#!python3
220+
221+
import os
222+
223+
print("myCred expires at " + os.getenv("GPTSCRIPT_CREDENTIAL_EXPIRATION", ""))
224+
```

integration/cred_test.go

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package integration
22

33
import (
4+
"strings"
45
"testing"
6+
"time"
57

68
"github.com/stretchr/testify/require"
79
)
@@ -15,15 +17,31 @@ func TestGPTScriptCredential(t *testing.T) {
1517
// TestCredentialScopes makes sure that environment variables set by credential tools and shared credential tools
1618
// are only available to the correct tools. See scripts/credscopes.gpt for more details.
1719
func TestCredentialScopes(t *testing.T) {
18-
out, err := RunScript("scripts/credscopes.gpt", "--sub-tool", "oneOne")
20+
out, err := RunScript("scripts/cred_scopes.gpt", "--sub-tool", "oneOne")
1921
require.NoError(t, err)
2022
require.Contains(t, out, "good")
2123

22-
out, err = RunScript("scripts/credscopes.gpt", "--sub-tool", "twoOne")
24+
out, err = RunScript("scripts/cred_scopes.gpt", "--sub-tool", "twoOne")
2325
require.NoError(t, err)
2426
require.Contains(t, out, "good")
2527

26-
out, err = RunScript("scripts/credscopes.gpt", "--sub-tool", "twoTwo")
28+
out, err = RunScript("scripts/cred_scopes.gpt", "--sub-tool", "twoTwo")
2729
require.NoError(t, err)
2830
require.Contains(t, out, "good")
2931
}
32+
33+
// TestCredentialExpirationEnv tests a GPTScript with two credentials that expire at different times.
34+
// One expires after two hours, and the other expires after one hour.
35+
// This test makes sure that the GPTSCRIPT_CREDENTIAL_EXPIRATION environment variable is set to the nearer expiration time (1h).
36+
func TestCredentialExpirationEnv(t *testing.T) {
37+
out, err := RunScript("scripts/cred_expiration.gpt")
38+
require.NoError(t, err)
39+
40+
for _, line := range strings.Split(out, "\n") {
41+
if timestamp, found := strings.CutPrefix(line, "Expires: "); found {
42+
expiresTime, err := time.Parse(time.RFC3339, timestamp)
43+
require.NoError(t, err)
44+
require.True(t, time.Until(expiresTime) < time.Hour)
45+
}
46+
}
47+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
cred: credentialTool with 2 as hours
2+
cred: credentialTool with 1 as hours
3+
4+
#!python3
5+
6+
import os
7+
8+
print("Expires: " + os.getenv("GPTSCRIPT_CREDENTIAL_EXPIRATION", ""), end="")
9+
10+
---
11+
name: credentialTool
12+
args: hours: the number of hours from now to expire
13+
14+
#!python3
15+
16+
import os
17+
import json
18+
from datetime import datetime, timedelta, timezone
19+
20+
class Output:
21+
def __init__(self, env, expires_at):
22+
self.env = env
23+
self.expiresAt = expires_at
24+
25+
def to_dict(self):
26+
return {
27+
"env": self.env,
28+
"expiresAt": self.expiresAt.isoformat()
29+
}
30+
31+
hours_str = os.getenv("HOURS")
32+
if hours_str is None:
33+
print("HOURS environment variable is not set")
34+
os._exit(1)
35+
36+
try:
37+
hours = int(hours_str)
38+
except ValueError:
39+
print("failed to parse HOURS")
40+
os._exit(1)
41+
42+
expires_at = datetime.now(timezone.utc) + timedelta(hours=hours)
43+
out = Output(env={"yeet": "yote"}, expires_at=expires_at)
44+
out_json = json.dumps(out.to_dict())
45+
46+
print(out_json)

pkg/config/cliconfig.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ type CLIConfig struct {
5555
Auths map[string]AuthConfig `json:"auths,omitempty"`
5656
CredentialsStore string `json:"credsStore,omitempty"`
5757
GPTScriptConfigFile string `json:"gptscriptConfig,omitempty"`
58+
GatewayURL string `json:"gatewayURL,omitempty"`
59+
Integrations map[string]string `json:"integrations,omitempty"`
5860

5961
auths map[string]types.AuthConfig
6062
authsLock *sync.Mutex

pkg/credentials/credential.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const (
1616
CredentialTypeTool CredentialType = "tool"
1717
CredentialTypeModelProvider CredentialType = "modelProvider"
1818
ExistingCredential = "GPTSCRIPT_EXISTING_CREDENTIAL"
19+
CredentialExpiration = "GPTSCRIPT_CREDENTIAL_EXPIRATION"
1920
)
2021

2122
type Credential struct {

pkg/runner/runner.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -865,6 +865,7 @@ func (r *Runner) handleCredentials(callCtx engine.Context, monitor Monitor, env
865865
}
866866
}
867867

868+
var nearestExpiration *time.Time
868869
for _, ref := range credToolRefs {
869870
toolName, credentialAlias, args, err := types.ParseCredentialArgs(ref.Reference, callCtx.Input)
870871
if err != nil {
@@ -967,11 +968,19 @@ func (r *Runner) handleCredentials(callCtx engine.Context, monitor Monitor, env
967968
} else {
968969
log.Warnf("Not saving credential for tool %s - credentials will only be saved for tools from GitHub, or tools that use aliases.", toolName)
969970
}
971+
972+
if c.ExpiresAt != nil && (nearestExpiration == nil || nearestExpiration.After(*c.ExpiresAt)) {
973+
nearestExpiration = c.ExpiresAt
974+
}
970975
}
971976

972977
for k, v := range c.Env {
973978
env = append(env, fmt.Sprintf("%s=%s", k, v))
974979
}
980+
981+
if nearestExpiration != nil {
982+
env = append(env, fmt.Sprintf("%s=%s", credentials.CredentialExpiration, nearestExpiration.Format(time.RFC3339)))
983+
}
975984
}
976985

977986
return env, nil

0 commit comments

Comments
 (0)