diff --git a/src/gptscript.ts b/src/gptscript.ts index 98d5dce..f8e84c7 100644 --- a/src/gptscript.ts +++ b/src/gptscript.ts @@ -43,6 +43,7 @@ export interface RunOpts { confirm?: boolean prompt?: boolean credentialOverrides?: string[] + credentialContexts?: string[] location?: string env?: string[] forceSequential?: boolean @@ -320,6 +321,47 @@ export class GPTScript { return this._load({toolDefs, disableCache, subTool}) } + async listCredentials(context: Array, allContexts: boolean): Promise> { + if (!this.ready) { + this.ready = await this.testGPTScriptURL(20) + } + + const r: Run = new RunSubcommand("credentials", "", {}, GPTScript.serverURL) + r.request({context, allContexts}) + const out = await r.json() + return out.map((c: any) => jsonToCredential(JSON.stringify(c))) + } + + async createCredential(credential: Credential): Promise { + if (!this.ready) { + this.ready = await this.testGPTScriptURL(20) + } + + const r: Run = new RunSubcommand("credentials/create", "", {}, GPTScript.serverURL) + r.request({content: credentialToJSON(credential)}) + await r.text() + } + + async revealCredential(context: Array, name: string): Promise { + if (!this.ready) { + this.ready = await this.testGPTScriptURL(20) + } + + const r: Run = new RunSubcommand("credentials/reveal", "", {}, GPTScript.serverURL) + r.request({context, name}) + return jsonToCredential(await r.text()) + } + + async deleteCredential(context: string, name: string): Promise { + if (!this.ready) { + this.ready = await this.testGPTScriptURL(20) + } + + const r: Run = new RunSubcommand("credentials/delete", "", {}, GPTScript.serverURL) + r.request({context: [context], name}) + await r.text() + } + /** * Helper method to handle the common logic for loading. * @@ -967,3 +1009,48 @@ function parseBlocksFromNodes(nodes: any[]): Block[] { function randomId(prefix: string): string { return prefix + Math.random().toString(36).substring(2, 12) } + +export enum CredentialType { + Tool = "tool", + ModelProvider = "modelProvider", +} + +export type Credential = { + context: string + name: string + type: CredentialType + env: Record + ephemeral: boolean + expiresAt?: Date | undefined + refreshToken?: string | undefined +} + +// for internal use only +type cred = { + context: string + toolName: string + type: string + env: Record + ephemeral: boolean + expiresAt: string | undefined + refreshToken: string | undefined +} + +export function credentialToJSON(c: Credential): string { + const expiresAt = c.expiresAt ? c.expiresAt.toISOString() : undefined + const type = c.type === CredentialType.Tool ? "tool" : "modelProvider" + return JSON.stringify({context: c.context, toolName: c.name, type: type, env: c.env, ephemeral: c.ephemeral, expiresAt: expiresAt, refreshToken: c.refreshToken} as cred) +} + +function jsonToCredential(cred: string): Credential { + const c = JSON.parse(cred) as cred + return { + context: c.context, + name: c.toolName, + type: c.type === "tool" ? CredentialType.Tool : CredentialType.ModelProvider, + env: c.env, + ephemeral: c.ephemeral, + expiresAt: c.expiresAt ? new Date(c.expiresAt) : undefined, + refreshToken: c.refreshToken + } +} diff --git a/tests/fixtures/global-tools.gpt b/tests/fixtures/global-tools.gpt index 0e5d0f6..6ad6eee 100644 --- a/tests/fixtures/global-tools.gpt +++ b/tests/fixtures/global-tools.gpt @@ -4,7 +4,7 @@ Runbook 3 --- Name: tool_1 -Global Tools: sys.read, sys.write, github.com/gptscript-ai/knowledge, github.com/drpebcak/duckdb, github.com/gptscript-ai/browser, github.com/gptscript-ai/browser-search/google, github.com/gptscript-ai/browser-search/google-question-answerer +Global Tools: sys.read, sys.write, github.com/drpebcak/duckdb, github.com/gptscript-ai/browser, github.com/gptscript-ai/browser-search/google, github.com/gptscript-ai/browser-search/google-question-answerer Say "Hello!" @@ -16,4 +16,4 @@ What time is it? --- Name: tool_3 -Give me a paragraph of lorem ipsum \ No newline at end of file +Give me a paragraph of lorem ipsum diff --git a/tests/gptscript.test.ts b/tests/gptscript.test.ts index d1ae8b8..f8450d1 100644 --- a/tests/gptscript.test.ts +++ b/tests/gptscript.test.ts @@ -1,8 +1,18 @@ import * as gptscript from "../src/gptscript" -import {ArgumentSchemaType, getEnv, PropertyType, RunEventType, TextType, ToolDef, ToolType} from "../src/gptscript" +import { + ArgumentSchemaType, + Credential, CredentialType, + getEnv, + PropertyType, + RunEventType, + TextType, + ToolDef, + ToolType +} from "../src/gptscript" import path from "path" import {fileURLToPath} from "url" import * as fs from "node:fs" +import {randomBytes} from "node:crypto"; let gFirst: gptscript.GPTScript let g: gptscript.GPTScript @@ -791,4 +801,59 @@ describe("gptscript module", () => { expect(err).toEqual(undefined) expect(out).toEqual("200") }, 20000) -}) \ No newline at end of file + + test("credential operations", async () => { + const name = "test-" + randomBytes(10).toString("hex") + const value = randomBytes(10).toString("hex") + + // Create + try { + await g.createCredential({ + name: name, + context: "default", + env: {"TEST": value}, + ephemeral: false, + expiresAt: new Date(Date.now() + 5000), // 5 seconds from now + type: CredentialType.Tool, + }) + } catch (e) { + throw new Error("failed to create credential: " + e) + } + + // Wait 5 seconds + await new Promise(resolve => setTimeout(resolve, 5000)) + + // Reveal + try { + const result = await g.revealCredential(["default"], name) + expect(result.env["TEST"]).toEqual(value) + expect(result.expiresAt!.valueOf()).toBeLessThan(new Date().valueOf()) + } catch (e) { + throw new Error("failed to reveal credential: " + e) + } + + // List + try { + const result = await g.listCredentials(["default"], false) + expect(result.length).toBeGreaterThan(0) + expect(result.map(c => c.name)).toContain(name) + } catch (e) { + throw new Error("failed to list credentials: " + e) + } + + // Delete + try { + await g.deleteCredential("default", name) + } catch (e) { + throw new Error("failed to delete credential: " + e) + } + + // Verify deletion + try { + const result = await g.listCredentials(["default"], false) + expect(result.map(c => c.name)).not.toContain(name) + } catch (e) { + throw new Error("failed to verify deletion: " + e) + } + }, 20000) +})