From 73aa542997ae132901fac9c3a87b402767c85a5e Mon Sep 17 00:00:00 2001 From: Grant Linville Date: Tue, 17 Sep 2024 11:00:39 -0400 Subject: [PATCH 1/7] WIP: credentials Signed-off-by: Grant Linville --- src/gptscript.ts | 109 ++++++++++++++++++++++++++++++++++++++++ tests/gptscript.test.ts | 64 ++++++++++++++++++++++- 2 files changed, 171 insertions(+), 2 deletions(-) diff --git a/src/gptscript.ts b/src/gptscript.ts index 98d5dce..50b9b0c 100644 --- a/src/gptscript.ts +++ b/src/gptscript.ts @@ -320,6 +320,70 @@ export class GPTScript { return this._load({toolDefs, disableCache, subTool}) } + async listCredentials(context: string, allContexts: boolean): Promise> { + if (!this.ready) { + this.ready = await this.testGPTScriptURL(20) + } + const resp = await fetch(`${GPTScript.serverURL}/credentials`, { + method: "POST", + body: JSON.stringify({context, allContexts}) + }) + + if (resp.status < 200 || resp.status >= 400) { + throw new Error(`Failed to list credentials: ${(await resp.json())["stderr"]}`) + } + + const r = await resp.json() + return r["stdout"].map((c: any) => jsonToCredential(JSON.stringify(c))) + } + + async createCredential(credential: Credential): Promise { + if (!this.ready) { + this.ready = await this.testGPTScriptURL(20) + } + const resp = await fetch(`${GPTScript.serverURL}/credentials/create`, { + method: "POST", + body: JSON.stringify({ + content: credentialToJSON(credential) + }) + }) + + if (resp.status < 200 || resp.status >= 400) { + throw new Error(`Failed to create credential: ${(await resp.json())["stderr"]}`) + } + } + + async revealCredential(context: string, name: string): Promise { + if (!this.ready) { + this.ready = await this.testGPTScriptURL(20) + } + const resp = await fetch(`${GPTScript.serverURL}/credentials/reveal`, { + method: "POST", + body: JSON.stringify({context, name}) + }) + + if (resp.status < 200 || resp.status >= 400) { + throw new Error(`Failed to reveal credential: ${(await resp.json())["stderr"]}`) + } + + const r = await resp.json() + return r["stdout"] as Credential + } + + async deleteCredential(context: string, name: string): Promise { + if (!this.ready) { + this.ready = await this.testGPTScriptURL(20) + } + const resp = await fetch(`${GPTScript.serverURL}/credentials/delete`, { + method: "POST", + body: JSON.stringify({context, name}) + }) + + if (resp.status < 200 || resp.status >= 400) { + throw new Error(`Failed to delete credential: ${(await resp.json())["stderr"]}`) + } + } + /** * Helper method to handle the common logic for loading. * @@ -967,3 +1031,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/gptscript.test.ts b/tests/gptscript.test.ts index d1ae8b8..1d3192d 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,54 @@ 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, + type: CredentialType.Tool, + }) + } catch (e) { + throw new Error("failed to create credential: " + e) + } + + // Reveal + try { + const result = await g.revealCredential("default", name) + expect(result.env["TEST"]).toEqual(value) + } 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) + } + }) +}) From bddb5a988c66eb7ab519712842edfab90553aefa Mon Sep 17 00:00:00 2001 From: Grant Linville Date: Wed, 18 Sep 2024 14:32:18 -0400 Subject: [PATCH 2/7] update for stacked contexts Signed-off-by: Grant Linville --- src/gptscript.ts | 8 ++++---- tests/gptscript.test.ts | 13 +++++++++---- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/gptscript.ts b/src/gptscript.ts index 50b9b0c..6fa23ff 100644 --- a/src/gptscript.ts +++ b/src/gptscript.ts @@ -320,7 +320,7 @@ export class GPTScript { return this._load({toolDefs, disableCache, subTool}) } - async listCredentials(context: string, allContexts: boolean): Promise> { + async listCredentials(context: Array, allContexts: boolean): Promise> { if (!this.ready) { this.ready = await this.testGPTScriptURL(20) } @@ -353,7 +353,7 @@ export class GPTScript { } } - async revealCredential(context: string, name: string): Promise { + async revealCredential(context: Array, name: string): Promise { if (!this.ready) { this.ready = await this.testGPTScriptURL(20) } @@ -367,7 +367,7 @@ export class GPTScript { } const r = await resp.json() - return r["stdout"] as Credential + return jsonToCredential(JSON.stringify(r["stdout"])) } async deleteCredential(context: string, name: string): Promise { @@ -376,7 +376,7 @@ export class GPTScript { } const resp = await fetch(`${GPTScript.serverURL}/credentials/delete`, { method: "POST", - body: JSON.stringify({context, name}) + body: JSON.stringify({context: [context], name}) }) if (resp.status < 200 || resp.status >= 400) { diff --git a/tests/gptscript.test.ts b/tests/gptscript.test.ts index 1d3192d..f8450d1 100644 --- a/tests/gptscript.test.ts +++ b/tests/gptscript.test.ts @@ -813,23 +813,28 @@ describe("gptscript module", () => { 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) + 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) + const result = await g.listCredentials(["default"], false) expect(result.length).toBeGreaterThan(0) expect(result.map(c => c.name)).toContain(name) } catch (e) { @@ -845,10 +850,10 @@ describe("gptscript module", () => { // Verify deletion try { - const result = await g.listCredentials("default", false) + 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) }) From 5fe499001050408f10e8d7dac1e4138e96793f18 Mon Sep 17 00:00:00 2001 From: Grant Linville Date: Wed, 18 Sep 2024 14:58:29 -0400 Subject: [PATCH 3/7] debug windows Signed-off-by: Grant Linville --- src/gptscript.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/gptscript.ts b/src/gptscript.ts index 6fa23ff..cccd62b 100644 --- a/src/gptscript.ts +++ b/src/gptscript.ts @@ -554,6 +554,7 @@ export class Run { reject(new Error(this.stderr)) } } else if (this.state === RunState.Error) { + console.error(this.err) reject(new Error(this.err)) } }) From 99f47d0191a51ab7438e477ef239d58e2e526860 Mon Sep 17 00:00:00 2001 From: Grant Linville Date: Wed, 18 Sep 2024 15:01:23 -0400 Subject: [PATCH 4/7] debug windows Signed-off-by: Grant Linville --- src/gptscript.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gptscript.ts b/src/gptscript.ts index cccd62b..bd36ba0 100644 --- a/src/gptscript.ts +++ b/src/gptscript.ts @@ -554,7 +554,7 @@ export class Run { reject(new Error(this.stderr)) } } else if (this.state === RunState.Error) { - console.error(this.err) + console.log(this.err) reject(new Error(this.err)) } }) From f0e2a46e9698eb54a69329f104c59e1759c89e21 Mon Sep 17 00:00:00 2001 From: Grant Linville Date: Wed, 18 Sep 2024 16:04:44 -0400 Subject: [PATCH 5/7] hopefully fix the test Signed-off-by: Grant Linville --- src/gptscript.ts | 1 - tests/fixtures/global-tools.gpt | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/gptscript.ts b/src/gptscript.ts index bd36ba0..6fa23ff 100644 --- a/src/gptscript.ts +++ b/src/gptscript.ts @@ -554,7 +554,6 @@ export class Run { reject(new Error(this.stderr)) } } else if (this.state === RunState.Error) { - console.log(this.err) reject(new Error(this.err)) } }) 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 From 2ed44c0090dedd99b8b8e90a98bc0c5c5029dc0d Mon Sep 17 00:00:00 2001 From: Grant Linville Date: Thu, 19 Sep 2024 17:05:08 -0400 Subject: [PATCH 6/7] PR feedback Signed-off-by: Grant Linville --- src/gptscript.ts | 49 +++++++++++++----------------------------------- 1 file changed, 13 insertions(+), 36 deletions(-) diff --git a/src/gptscript.ts b/src/gptscript.ts index 6fa23ff..ef9d56a 100644 --- a/src/gptscript.ts +++ b/src/gptscript.ts @@ -324,64 +324,41 @@ export class GPTScript { if (!this.ready) { this.ready = await this.testGPTScriptURL(20) } - const resp = await fetch(`${GPTScript.serverURL}/credentials`, { - method: "POST", - body: JSON.stringify({context, allContexts}) - }) - - if (resp.status < 200 || resp.status >= 400) { - throw new Error(`Failed to list credentials: ${(await resp.json())["stderr"]}`) - } - const r = await resp.json() - return r["stdout"].map((c: any) => jsonToCredential(JSON.stringify(c))) + 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 resp = await fetch(`${GPTScript.serverURL}/credentials/create`, { - method: "POST", - body: JSON.stringify({ - content: credentialToJSON(credential) - }) - }) - if (resp.status < 200 || resp.status >= 400) { - throw new Error(`Failed to create credential: ${(await resp.json())["stderr"]}`) - } + 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 resp = await fetch(`${GPTScript.serverURL}/credentials/reveal`, { - method: "POST", - body: JSON.stringify({context, name}) - }) - - if (resp.status < 200 || resp.status >= 400) { - throw new Error(`Failed to reveal credential: ${(await resp.json())["stderr"]}`) - } - const r = await resp.json() - return jsonToCredential(JSON.stringify(r["stdout"])) + 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 resp = await fetch(`${GPTScript.serverURL}/credentials/delete`, { - method: "POST", - body: JSON.stringify({context: [context], name}) - }) - if (resp.status < 200 || resp.status >= 400) { - throw new Error(`Failed to delete credential: ${(await resp.json())["stderr"]}`) - } + const r: Run = new RunSubcommand("credentials/delete", "", {}, GPTScript.serverURL) + r.request({context: [context], name}) + await r.text() } /** From d8db7024d32cd62a73f046a7d55104cd86839fbe Mon Sep 17 00:00:00 2001 From: Grant Linville Date: Fri, 20 Sep 2024 14:11:02 -0400 Subject: [PATCH 7/7] add credentialContexts to RunOpts Signed-off-by: Grant Linville --- src/gptscript.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/gptscript.ts b/src/gptscript.ts index ef9d56a..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