Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add credential management #91

Merged
merged 7 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions src/gptscript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,47 @@ export class GPTScript {
return this._load({toolDefs, disableCache, subTool})
}

async listCredentials(context: Array<string>, allContexts: boolean): Promise<Array<Credential>> {
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<void> {
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<string>, name: string): Promise<Credential> {
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<void> {
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.
*
Expand Down Expand Up @@ -967,3 +1008,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<string, string>
ephemeral: boolean
expiresAt?: Date | undefined
refreshToken?: string | undefined
}

// for internal use only
type cred = {
context: string
toolName: string
type: string
env: Record<string, string>
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
}
}
4 changes: 2 additions & 2 deletions tests/fixtures/global-tools.gpt
Original file line number Diff line number Diff line change
Expand Up @@ -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!"

Expand All @@ -16,4 +16,4 @@ What time is it?
---
Name: tool_3

Give me a paragraph of lorem ipsum
Give me a paragraph of lorem ipsum
69 changes: 67 additions & 2 deletions tests/gptscript.test.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -791,4 +801,59 @@ describe("gptscript module", () => {
expect(err).toEqual(undefined)
expect(out).toEqual("200")
}, 20000)
})

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)
})