From b22b213ddffde4a2f2d2f4821af36e7be7510d0f Mon Sep 17 00:00:00 2001 From: Grant Linville Date: Mon, 5 Aug 2024 16:38:50 -0400 Subject: [PATCH 1/3] enhance: credentials: add GPTSCRIPT_CREDENTIAL_EXPIRATION Signed-off-by: Grant Linville --- integration/cred_test.go | 24 ++++++++-- integration/scripts/cred_expiration.gpt | 46 +++++++++++++++++++ .../{credscopes.gpt => cred_scopes.gpt} | 0 pkg/config/cliconfig.go | 2 + pkg/credentials/credential.go | 1 + pkg/runner/runner.go | 9 ++++ 6 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 integration/scripts/cred_expiration.gpt rename integration/scripts/{credscopes.gpt => cred_scopes.gpt} (100%) diff --git a/integration/cred_test.go b/integration/cred_test.go index 67298ef8..d77f096c 100644 --- a/integration/cred_test.go +++ b/integration/cred_test.go @@ -1,7 +1,9 @@ package integration import ( + "strings" "testing" + "time" "github.com/stretchr/testify/require" ) @@ -15,15 +17,31 @@ func TestGPTScriptCredential(t *testing.T) { // TestCredentialScopes makes sure that environment variables set by credential tools and shared credential tools // are only available to the correct tools. See scripts/credscopes.gpt for more details. func TestCredentialScopes(t *testing.T) { - out, err := RunScript("scripts/credscopes.gpt", "--sub-tool", "oneOne") + out, err := RunScript("scripts/cred_scopes.gpt", "--sub-tool", "oneOne") require.NoError(t, err) require.Contains(t, out, "good") - out, err = RunScript("scripts/credscopes.gpt", "--sub-tool", "twoOne") + out, err = RunScript("scripts/cred_scopes.gpt", "--sub-tool", "twoOne") require.NoError(t, err) require.Contains(t, out, "good") - out, err = RunScript("scripts/credscopes.gpt", "--sub-tool", "twoTwo") + out, err = RunScript("scripts/cred_scopes.gpt", "--sub-tool", "twoTwo") require.NoError(t, err) require.Contains(t, out, "good") } + +// TestCredentialExpirationEnv tests a GPTScript with two credentials that expire at different times. +// One expires after two hours, and the other expires after one hour. +// This test makes sure that the GPTSCRIPT_CREDENTIAL_EXPIRATION environment variable is set to the nearer expiration time (1h). +func TestCredentialExpirationEnv(t *testing.T) { + out, err := RunScript("scripts/cred_expiration.gpt") + require.NoError(t, err) + + for _, line := range strings.Split(out, "\n") { + if timestamp, found := strings.CutPrefix(line, "Expires: "); found { + expiresTime, err := time.Parse(time.RFC3339, timestamp) + require.NoError(t, err) + require.True(t, time.Until(expiresTime) < time.Hour) + } + } +} diff --git a/integration/scripts/cred_expiration.gpt b/integration/scripts/cred_expiration.gpt new file mode 100644 index 00000000..40df8aaa --- /dev/null +++ b/integration/scripts/cred_expiration.gpt @@ -0,0 +1,46 @@ +cred: credentialTool with 2 as hours +cred: credentialTool with 1 as hours + +#!python3 + +import os + +print("Expires: " + os.getenv("GPTSCRIPT_CREDENTIAL_EXPIRATION", "")) + +--- +name: credentialTool +args: hours: the number of hours from now to expire + +#!python3 + +import os +import json +from datetime import datetime, timedelta, timezone + +class Output: + def __init__(self, env, expires_at): + self.env = env + self.expiresAt = expires_at + + def to_dict(self): + return { + "env": self.env, + "expiresAt": self.expiresAt.isoformat() + } + +hours_str = os.getenv("HOURS") +if hours_str is None: + print("HOURS environment variable is not set") + os._exit(1) + +try: + hours = int(hours_str) +except ValueError: + print("failed to parse HOURS") + os._exit(1) + +expires_at = datetime.now(timezone.utc) + timedelta(hours=hours) +out = Output(env={"yeet": "yote"}, expires_at=expires_at) +out_json = json.dumps(out.to_dict()) + +print(out_json) diff --git a/integration/scripts/credscopes.gpt b/integration/scripts/cred_scopes.gpt similarity index 100% rename from integration/scripts/credscopes.gpt rename to integration/scripts/cred_scopes.gpt diff --git a/pkg/config/cliconfig.go b/pkg/config/cliconfig.go index e4aa49ab..7970415f 100644 --- a/pkg/config/cliconfig.go +++ b/pkg/config/cliconfig.go @@ -55,6 +55,8 @@ type CLIConfig struct { Auths map[string]AuthConfig `json:"auths,omitempty"` CredentialsStore string `json:"credsStore,omitempty"` GPTScriptConfigFile string `json:"gptscriptConfig,omitempty"` + GatewayURL string `json:"gatewayURL,omitempty"` + Integrations map[string]string `json:"integrations,omitempty"` auths map[string]types.AuthConfig authsLock *sync.Mutex diff --git a/pkg/credentials/credential.go b/pkg/credentials/credential.go index 605208a0..3d1e2192 100644 --- a/pkg/credentials/credential.go +++ b/pkg/credentials/credential.go @@ -16,6 +16,7 @@ const ( CredentialTypeTool CredentialType = "tool" CredentialTypeModelProvider CredentialType = "modelProvider" ExistingCredential = "GPTSCRIPT_EXISTING_CREDENTIAL" + CredentialExpiration = "GPTSCRIPT_CREDENTIAL_EXPIRATION" ) type Credential struct { diff --git a/pkg/runner/runner.go b/pkg/runner/runner.go index 3a33c720..c2137bea 100644 --- a/pkg/runner/runner.go +++ b/pkg/runner/runner.go @@ -865,6 +865,7 @@ func (r *Runner) handleCredentials(callCtx engine.Context, monitor Monitor, env } } + var nearestExpiration *time.Time for _, ref := range credToolRefs { toolName, credentialAlias, args, err := types.ParseCredentialArgs(ref.Reference, callCtx.Input) if err != nil { @@ -967,11 +968,19 @@ func (r *Runner) handleCredentials(callCtx engine.Context, monitor Monitor, env } else { log.Warnf("Not saving credential for tool %s - credentials will only be saved for tools from GitHub, or tools that use aliases.", toolName) } + + if c.ExpiresAt != nil && (nearestExpiration == nil || nearestExpiration.After(*c.ExpiresAt)) { + nearestExpiration = c.ExpiresAt + } } for k, v := range c.Env { env = append(env, fmt.Sprintf("%s=%s", k, v)) } + + if nearestExpiration != nil { + env = append(env, fmt.Sprintf("%s=%s", credentials.CredentialExpiration, nearestExpiration.Format(time.RFC3339))) + } } return env, nil From a5b4366eeb4cd7205739a5a9c4744f67edf052a0 Mon Sep 17 00:00:00 2001 From: Grant Linville Date: Mon, 5 Aug 2024 16:46:49 -0400 Subject: [PATCH 2/3] fix Signed-off-by: Grant Linville --- integration/scripts/cred_expiration.gpt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration/scripts/cred_expiration.gpt b/integration/scripts/cred_expiration.gpt index 40df8aaa..da535df0 100644 --- a/integration/scripts/cred_expiration.gpt +++ b/integration/scripts/cred_expiration.gpt @@ -5,7 +5,7 @@ cred: credentialTool with 1 as hours import os -print("Expires: " + os.getenv("GPTSCRIPT_CREDENTIAL_EXPIRATION", "")) +print("Expires: " + os.getenv("GPTSCRIPT_CREDENTIAL_EXPIRATION", ""), end="") --- name: credentialTool From ac93d5fea90c7e509a24d9451ed0414e9be86f29 Mon Sep 17 00:00:00 2001 From: Grant Linville Date: Mon, 5 Aug 2024 16:59:59 -0400 Subject: [PATCH 3/3] update doc Signed-off-by: Grant Linville --- docs/docs/03-tools/04-credential-tools.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/docs/03-tools/04-credential-tools.md b/docs/docs/03-tools/04-credential-tools.md index 3e6a678a..1911dc34 100644 --- a/docs/docs/03-tools/04-credential-tools.md +++ b/docs/docs/03-tools/04-credential-tools.md @@ -204,3 +204,21 @@ that environment variable, and if it is set, get the refresh token from the exis typically without user interaction. For an example of a tool that uses the refresh feature, see the [Gateway OAuth2 tool](https://github.com/gptscript-ai/gateway-oauth2). + +### GPTSCRIPT_CREDENTIAL_EXPIRATION environment variable + +When a tool references a credential tool, GPTScript will add the environment variables from the credential to the tool's +environment before executing the tool. If at least one of the credentials has an `expiresAt` field, GPTScript will also +set the environment variable `GPTSCRIPT_CREDENTIAL_EXPIRATION`, which contains the nearest expiration time out of all +credentials referenced by the tool, in RFC 3339 format. That way, it can be referenced in the tool body if needed. +Here is an example: + +``` +Credential: my-credential-tool.gpt as myCred + +#!python3 + +import os + +print("myCred expires at " + os.getenv("GPTSCRIPT_CREDENTIAL_EXPIRATION", "")) +```