Skip to content

Commit 92646be

Browse files
authored
feat: add credential management (#91)
Signed-off-by: Grant Linville <[email protected]>
1 parent 7af9fd5 commit 92646be

File tree

3 files changed

+156
-4
lines changed

3 files changed

+156
-4
lines changed

src/gptscript.ts

+87
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export interface RunOpts {
4343
confirm?: boolean
4444
prompt?: boolean
4545
credentialOverrides?: string[]
46+
credentialContexts?: string[]
4647
location?: string
4748
env?: string[]
4849
forceSequential?: boolean
@@ -320,6 +321,47 @@ export class GPTScript {
320321
return this._load({toolDefs, disableCache, subTool})
321322
}
322323

324+
async listCredentials(context: Array<string>, allContexts: boolean): Promise<Array<Credential>> {
325+
if (!this.ready) {
326+
this.ready = await this.testGPTScriptURL(20)
327+
}
328+
329+
const r: Run = new RunSubcommand("credentials", "", {}, GPTScript.serverURL)
330+
r.request({context, allContexts})
331+
const out = await r.json()
332+
return out.map((c: any) => jsonToCredential(JSON.stringify(c)))
333+
}
334+
335+
async createCredential(credential: Credential): Promise<void> {
336+
if (!this.ready) {
337+
this.ready = await this.testGPTScriptURL(20)
338+
}
339+
340+
const r: Run = new RunSubcommand("credentials/create", "", {}, GPTScript.serverURL)
341+
r.request({content: credentialToJSON(credential)})
342+
await r.text()
343+
}
344+
345+
async revealCredential(context: Array<string>, name: string): Promise<Credential> {
346+
if (!this.ready) {
347+
this.ready = await this.testGPTScriptURL(20)
348+
}
349+
350+
const r: Run = new RunSubcommand("credentials/reveal", "", {}, GPTScript.serverURL)
351+
r.request({context, name})
352+
return jsonToCredential(await r.text())
353+
}
354+
355+
async deleteCredential(context: string, name: string): Promise<void> {
356+
if (!this.ready) {
357+
this.ready = await this.testGPTScriptURL(20)
358+
}
359+
360+
const r: Run = new RunSubcommand("credentials/delete", "", {}, GPTScript.serverURL)
361+
r.request({context: [context], name})
362+
await r.text()
363+
}
364+
323365
/**
324366
* Helper method to handle the common logic for loading.
325367
*
@@ -967,3 +1009,48 @@ function parseBlocksFromNodes(nodes: any[]): Block[] {
9671009
function randomId(prefix: string): string {
9681010
return prefix + Math.random().toString(36).substring(2, 12)
9691011
}
1012+
1013+
export enum CredentialType {
1014+
Tool = "tool",
1015+
ModelProvider = "modelProvider",
1016+
}
1017+
1018+
export type Credential = {
1019+
context: string
1020+
name: string
1021+
type: CredentialType
1022+
env: Record<string, string>
1023+
ephemeral: boolean
1024+
expiresAt?: Date | undefined
1025+
refreshToken?: string | undefined
1026+
}
1027+
1028+
// for internal use only
1029+
type cred = {
1030+
context: string
1031+
toolName: string
1032+
type: string
1033+
env: Record<string, string>
1034+
ephemeral: boolean
1035+
expiresAt: string | undefined
1036+
refreshToken: string | undefined
1037+
}
1038+
1039+
export function credentialToJSON(c: Credential): string {
1040+
const expiresAt = c.expiresAt ? c.expiresAt.toISOString() : undefined
1041+
const type = c.type === CredentialType.Tool ? "tool" : "modelProvider"
1042+
return JSON.stringify({context: c.context, toolName: c.name, type: type, env: c.env, ephemeral: c.ephemeral, expiresAt: expiresAt, refreshToken: c.refreshToken} as cred)
1043+
}
1044+
1045+
function jsonToCredential(cred: string): Credential {
1046+
const c = JSON.parse(cred) as cred
1047+
return {
1048+
context: c.context,
1049+
name: c.toolName,
1050+
type: c.type === "tool" ? CredentialType.Tool : CredentialType.ModelProvider,
1051+
env: c.env,
1052+
ephemeral: c.ephemeral,
1053+
expiresAt: c.expiresAt ? new Date(c.expiresAt) : undefined,
1054+
refreshToken: c.refreshToken
1055+
}
1056+
}

tests/fixtures/global-tools.gpt

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Runbook 3
44

55
---
66
Name: tool_1
7-
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
7+
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
88

99
Say "Hello!"
1010

@@ -16,4 +16,4 @@ What time is it?
1616
---
1717
Name: tool_3
1818

19-
Give me a paragraph of lorem ipsum
19+
Give me a paragraph of lorem ipsum

tests/gptscript.test.ts

+67-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
11
import * as gptscript from "../src/gptscript"
2-
import {ArgumentSchemaType, getEnv, PropertyType, RunEventType, TextType, ToolDef, ToolType} from "../src/gptscript"
2+
import {
3+
ArgumentSchemaType,
4+
Credential, CredentialType,
5+
getEnv,
6+
PropertyType,
7+
RunEventType,
8+
TextType,
9+
ToolDef,
10+
ToolType
11+
} from "../src/gptscript"
312
import path from "path"
413
import {fileURLToPath} from "url"
514
import * as fs from "node:fs"
15+
import {randomBytes} from "node:crypto";
616

717
let gFirst: gptscript.GPTScript
818
let g: gptscript.GPTScript
@@ -791,4 +801,59 @@ describe("gptscript module", () => {
791801
expect(err).toEqual(undefined)
792802
expect(out).toEqual("200")
793803
}, 20000)
794-
})
804+
805+
test("credential operations", async () => {
806+
const name = "test-" + randomBytes(10).toString("hex")
807+
const value = randomBytes(10).toString("hex")
808+
809+
// Create
810+
try {
811+
await g.createCredential({
812+
name: name,
813+
context: "default",
814+
env: {"TEST": value},
815+
ephemeral: false,
816+
expiresAt: new Date(Date.now() + 5000), // 5 seconds from now
817+
type: CredentialType.Tool,
818+
})
819+
} catch (e) {
820+
throw new Error("failed to create credential: " + e)
821+
}
822+
823+
// Wait 5 seconds
824+
await new Promise(resolve => setTimeout(resolve, 5000))
825+
826+
// Reveal
827+
try {
828+
const result = await g.revealCredential(["default"], name)
829+
expect(result.env["TEST"]).toEqual(value)
830+
expect(result.expiresAt!.valueOf()).toBeLessThan(new Date().valueOf())
831+
} catch (e) {
832+
throw new Error("failed to reveal credential: " + e)
833+
}
834+
835+
// List
836+
try {
837+
const result = await g.listCredentials(["default"], false)
838+
expect(result.length).toBeGreaterThan(0)
839+
expect(result.map(c => c.name)).toContain(name)
840+
} catch (e) {
841+
throw new Error("failed to list credentials: " + e)
842+
}
843+
844+
// Delete
845+
try {
846+
await g.deleteCredential("default", name)
847+
} catch (e) {
848+
throw new Error("failed to delete credential: " + e)
849+
}
850+
851+
// Verify deletion
852+
try {
853+
const result = await g.listCredentials(["default"], false)
854+
expect(result.map(c => c.name)).not.toContain(name)
855+
} catch (e) {
856+
throw new Error("failed to verify deletion: " + e)
857+
}
858+
}, 20000)
859+
})

0 commit comments

Comments
 (0)