From d09febf1fa2d72ff2825a44f1969c58955e8af0a Mon Sep 17 00:00:00 2001 From: Hugo Dutka Date: Tue, 2 Dec 2025 20:17:51 +0100 Subject: [PATCH] feat: handle undefined github app context in scout --- packages/scout-agent/agent.ts | 2 +- packages/scout-agent/lib/compute/tools.ts | 15 +++----- packages/scout-agent/lib/core.test.ts | 10 +++--- packages/scout-agent/lib/core.ts | 43 ++++++++++------------- packages/scout-agent/lib/github.test.ts | 19 +++++----- packages/scout-agent/lib/github.ts | 13 +++---- packages/scout-agent/package.json | 2 +- 7 files changed, 45 insertions(+), 59 deletions(-) diff --git a/packages/scout-agent/agent.ts b/packages/scout-agent/agent.ts index dcf2ae3..f59da0f 100644 --- a/packages/scout-agent/agent.ts +++ b/packages/scout-agent/agent.ts @@ -36,7 +36,7 @@ agent.on("request", async (request) => { }); agent.on("chat", async ({ id, messages }) => { - const params = scout.buildStreamTextParams({ + const params = await scout.buildStreamTextParams({ messages, chatID: id, model: "anthropic/claude-sonnet-4.5", diff --git a/packages/scout-agent/lib/compute/tools.ts b/packages/scout-agent/lib/compute/tools.ts index d426410..541e1c2 100644 --- a/packages/scout-agent/lib/compute/tools.ts +++ b/packages/scout-agent/lib/compute/tools.ts @@ -9,7 +9,7 @@ import { WORKSPACE_INFO_KEY } from "./common"; export const createComputeTools = ({ agent, - getGithubAppContext, + githubAppContext, initializeWorkspace, createWorkspaceClient, }: { @@ -19,10 +19,10 @@ export const createComputeTools = ({ ) => Promise<{ workspaceInfo: T; message: string }>; createWorkspaceClient: (workspaceInfo: T) => Promise; /** - * A function that returns the GitHub auth context for Git authentication. + * The GitHub auth context for Git authentication. * If provided, the workspace_authenticate_git tool will be available. */ - getGithubAppContext?: () => Promise; + githubAppContext?: github.AppAuthOptions; }): Record => { const newClient = async () => { const workspaceInfo = await agent.store.get(WORKSPACE_INFO_KEY); @@ -56,7 +56,7 @@ export const createComputeTools = ({ }, }), - ...(getGithubAppContext + ...(githubAppContext ? { workspace_authenticate_git: tool({ description: `Authenticate with Git repositories for push/pull operations. Call this before any Git operations that require authentication. @@ -74,13 +74,6 @@ It's safe to call this multiple times - re-authenticating is perfectly fine and execute: async (args, _opts) => { const client = await newClient(); - // Here we generate a GitHub token scoped to the repositories. - const githubAppContext = await getGithubAppContext(); - if (!githubAppContext) { - throw new Error( - "You can only use public repositories in this context." - ); - } const token = await github.authenticateApp({ ...githubAppContext, // TODO: We obviously need to handle owner at some point. diff --git a/packages/scout-agent/lib/core.test.ts b/packages/scout-agent/lib/core.test.ts index abfd203..e3a65ff 100644 --- a/packages/scout-agent/lib/core.test.ts +++ b/packages/scout-agent/lib/core.test.ts @@ -67,7 +67,7 @@ const newAgent = (options: { return new Response("Hello, world!", { status: 200 }); }); agent.on("chat", async ({ messages }) => { - const params = core.buildStreamTextParams({ + const params = await core.buildStreamTextParams({ model: options.model, messages, chatID: "b485db32-3d53-45fb-b980-6f4672fc66a6", @@ -363,7 +363,7 @@ test("buildStreamTextParams honors getGithubAppContext param", async () => { }, }); - const params = scout.buildStreamTextParams({ + const params = await scout.buildStreamTextParams({ chatID: "test-chat-id" as blink.ID, messages: [], model: newMockModel({ textResponse: "test" }), @@ -576,7 +576,7 @@ describe("daytona integration", () => { }, }); - const params = scout.buildStreamTextParams({ + const params = await scout.buildStreamTextParams({ chatID: "test-chat-id" as blink.ID, messages: [], model: newMockModel({ textResponse: "test" }), @@ -643,7 +643,7 @@ describe("daytona integration", () => { }, }); - const params = scout.buildStreamTextParams({ + const params = await scout.buildStreamTextParams({ chatID: "test-chat-id" as blink.ID, messages: [], model: newMockModel({ textResponse: "test" }), @@ -735,7 +735,7 @@ describe("daytona integration", () => { }, }); - const params = scout.buildStreamTextParams({ + const params = await scout.buildStreamTextParams({ chatID: "test-chat-id" as blink.ID, messages: [], model: newMockModel({ textResponse: "test" }), diff --git a/packages/scout-agent/lib/core.ts b/packages/scout-agent/lib/core.ts index 1632750..634d61a 100644 --- a/packages/scout-agent/lib/core.ts +++ b/packages/scout-agent/lib/core.ts @@ -47,7 +47,7 @@ export interface BuildStreamTextParamsOptions { * A function that returns the GitHub auth context for the GitHub tools and for Git authentication inside workspaces. * If not provided, the GitHub auth context will be created using the app ID and private key from the GitHub config. */ - getGithubAppContext?: () => Promise; + getGithubAppContext?: () => Promise; } interface Logger { @@ -264,7 +264,7 @@ export class Scout { } } - buildStreamTextParams({ + async buildStreamTextParams({ messages, chatID, model, @@ -272,15 +272,26 @@ export class Scout { tools: providedTools, getGithubAppContext, systemPrompt = defaultSystemPrompt, - }: BuildStreamTextParamsOptions): { + }: BuildStreamTextParamsOptions): Promise<{ model: LanguageModel; messages: ModelMessage[]; maxOutputTokens: number; providerOptions?: ProviderOptions; tools: Tools; - } { + }> { this.printConfigWarnings(); + // Resolve the GitHub app context once for all tools + const githubAppContext = this.github.config + ? await ( + getGithubAppContext ?? + githubAppContextFactory({ + appId: this.github.config.appID, + privateKey: this.github.config.privateKey, + }) + )() + : undefined; + const slackMetadata = getSlackMetadata(messages); const respondingInSlack = this.slack.app !== undefined && slackMetadata !== undefined; @@ -291,13 +302,7 @@ export class Scout { case "docker": { computeTools = createComputeTools({ agent: this.agent, - getGithubAppContext: this.github.config - ? (getGithubAppContext ?? - githubAppContextFactory({ - appId: this.github.config.appID, - privateKey: this.github.config.privateKey, - })) - : undefined, + githubAppContext, initializeWorkspace: initializeDockerWorkspace, createWorkspaceClient: getDockerWorkspaceClient, }); @@ -307,13 +312,7 @@ export class Scout { const opts = computeConfig.options; computeTools = createComputeTools({ agent: this.agent, - getGithubAppContext: this.github.config - ? (getGithubAppContext ?? - githubAppContextFactory({ - appId: this.github.config.appID, - privateKey: this.github.config.privateKey, - })) - : undefined, + githubAppContext, initializeWorkspace: (info) => initializeDaytonaWorkspace( this.logger, @@ -363,13 +362,7 @@ export class Scout { ? createGitHubTools({ agent: this.agent, chatID, - getGithubAppContext: - getGithubAppContext !== undefined - ? getGithubAppContext - : githubAppContextFactory({ - appId: this.github.config.appID, - privateKey: this.github.config.privateKey, - }), + githubAppContext, }) : undefined), ...computeTools, diff --git a/packages/scout-agent/lib/github.test.ts b/packages/scout-agent/lib/github.test.ts index 59ef448..5a00cbf 100644 --- a/packages/scout-agent/lib/github.test.ts +++ b/packages/scout-agent/lib/github.test.ts @@ -187,13 +187,12 @@ const withGitHubBotLogin = (login: string) => { return withEnvVariable("GITHUB_BOT_LOGIN", login); }; -const getGithubAppContextFactory = - (args: { appId: string; privateKey: string }) => async () => { - return { - appId: args.appId, - privateKey: Buffer.from(args.privateKey, "base64").toString("utf-8"), - }; +const makeGithubAppContext = (args: { appId: string; privateKey: string }) => { + return { + appId: args.appId, + privateKey: Buffer.from(args.privateKey, "base64").toString("utf-8"), }; +}; describe("defaultGetGithubAppContextFactory", () => { test("decodes base64 private key", async () => { @@ -248,7 +247,7 @@ describe("createGitHubTools", () => { const tools = createGitHubTools({ agent, chatID: "test-chat-id" as blink.ID, - getGithubAppContext: getGithubAppContextFactory({ + githubAppContext: makeGithubAppContext({ appId: "app-id", privateKey: Buffer.from("key").toString("base64"), }), @@ -317,7 +316,7 @@ describe("createGitHubTools", () => { const tools = createGitHubTools({ agent, chatID, - getGithubAppContext: getGithubAppContextFactory({ + githubAppContext: makeGithubAppContext({ appId: "12345", privateKey: TEST_RSA_PRIVATE_KEY_BASE64, }), @@ -407,7 +406,7 @@ describe("createGitHubTools", () => { const tools = createGitHubTools({ agent, chatID: "chat-id" as blink.ID, - getGithubAppContext: getGithubAppContextFactory({ + githubAppContext: makeGithubAppContext({ appId: "12345", privateKey: TEST_RSA_PRIVATE_KEY_BASE64, }), @@ -508,7 +507,7 @@ describe("createGitHubTools", () => { const tools = createGitHubTools({ agent, chatID: "chat" as blink.ID, - getGithubAppContext: getGithubAppContextFactory({ + githubAppContext: makeGithubAppContext({ appId: "12345", privateKey: TEST_RSA_PRIVATE_KEY_BASE64, }), diff --git a/packages/scout-agent/lib/github.ts b/packages/scout-agent/lib/github.ts index 42fcc1d..7f9b8ba 100644 --- a/packages/scout-agent/lib/github.ts +++ b/packages/scout-agent/lib/github.ts @@ -22,17 +22,19 @@ export const githubAppContextFactory = ({ export const createGitHubTools = ({ agent, chatID, - getGithubAppContext, + githubAppContext, }: { agent: blink.Agent; chatID: blink.ID; - getGithubAppContext: () => Promise; + githubAppContext: github.AppAuthOptions | undefined; }): Record => { return { ...blink.tools.prefix( - blink.tools.withContext(github.tools, { - appAuth: getGithubAppContext, - }), + githubAppContext + ? blink.tools.withContext(github.tools, { + appAuth: githubAppContext, + }) + : github.tools, "github_" ), @@ -40,7 +42,6 @@ export const createGitHubTools = ({ description: github.tools.create_pull_request.description, inputSchema: github.tools.create_pull_request.inputSchema, execute: async (args, { abortSignal }) => { - const githubAppContext = await getGithubAppContext(); if (!githubAppContext) { throw new Error( "You are not authorized to use this tool in this context." diff --git a/packages/scout-agent/package.json b/packages/scout-agent/package.json index c2899eb..7478497 100644 --- a/packages/scout-agent/package.json +++ b/packages/scout-agent/package.json @@ -1,7 +1,7 @@ { "name": "@blink-sdk/scout-agent", "description": "A general-purpose AI agent with GitHub, Slack, web search, and compute capabilities built on Blink SDK.", - "version": "0.0.5", + "version": "0.0.6", "type": "module", "keywords": [ "blink",