From 3c3bf5e70275c090cc6b72809d3b29cdecb20e8a Mon Sep 17 00:00:00 2001 From: Nikhil Sonti Date: Wed, 22 Oct 2025 17:48:42 -0700 Subject: [PATCH] get API key from browseros config --- .env.example | 2 +- packages/agent/src/agent/BaseAgent.ts | 102 ++++++++++++++------- packages/agent/src/agent/ClaudeSDKAgent.ts | 54 ++++++++++- packages/common/src/gateway.ts | 49 ++++++++++ packages/common/src/index.ts | 2 + packages/server/src/main.ts | 9 +- 6 files changed, 171 insertions(+), 47 deletions(-) create mode 100644 packages/common/src/gateway.ts diff --git a/.env.example b/.env.example index f5a8f80..0f68a17 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ -# Required +BROWSEROS_CONFIG_URL= ANTHROPIC_API_KEY= # Server Ports diff --git a/packages/agent/src/agent/BaseAgent.ts b/packages/agent/src/agent/BaseAgent.ts index c06cf5a..9f34dda 100644 --- a/packages/agent/src/agent/BaseAgent.ts +++ b/packages/agent/src/agent/BaseAgent.ts @@ -3,16 +3,16 @@ * Copyright 2025 BrowserOS */ -import { logger } from '@browseros/common' -import type { AgentConfig, AgentMetadata } from './types.js' -import type { FormattedEvent } from '../utils/EventFormatter.js' +import {logger} from '@browseros/common'; +import type {AgentConfig, AgentMetadata} from './types.js'; +import type {FormattedEvent} from '../utils/EventFormatter.js'; /** * Generic default system prompt for agents * * Minimal prompt - agents should override with their own specific prompts */ -export const DEFAULT_SYSTEM_PROMPT = `You are a browser automation agent.` +export const DEFAULT_SYSTEM_PROMPT = `You are a browser automation agent.`; /** * Generic default configuration values @@ -24,8 +24,8 @@ export const DEFAULT_CONFIG = { maxThinkingTokens: 10000, systemPrompt: DEFAULT_SYSTEM_PROMPT, mcpServers: {}, - permissionMode: 'bypassPermissions' as const -} + permissionMode: 'bypassPermissions' as const, +}; /** * BaseAgent - Abstract base class for all agent implementations @@ -56,26 +56,40 @@ export const DEFAULT_CONFIG = { * } */ export abstract class BaseAgent { - protected config: Required - protected metadata: AgentMetadata - protected executionStartTime: number = 0 + protected config: Required; + protected metadata: AgentMetadata; + protected executionStartTime: number = 0; + protected initialized: boolean = false; constructor( agentType: string, config: AgentConfig, - agentDefaults?: Partial + agentDefaults?: Partial, ) { // Merge config with agent-specific defaults, then with base defaults this.config = { apiKey: config.apiKey, cwd: config.cwd, - maxTurns: config.maxTurns ?? agentDefaults?.maxTurns ?? DEFAULT_CONFIG.maxTurns, - maxThinkingTokens: config.maxThinkingTokens ?? agentDefaults?.maxThinkingTokens ?? DEFAULT_CONFIG.maxThinkingTokens, - systemPrompt: config.systemPrompt ?? agentDefaults?.systemPrompt ?? DEFAULT_CONFIG.systemPrompt, - mcpServers: config.mcpServers ?? agentDefaults?.mcpServers ?? DEFAULT_CONFIG.mcpServers, - permissionMode: config.permissionMode ?? agentDefaults?.permissionMode ?? DEFAULT_CONFIG.permissionMode, - customOptions: config.customOptions ?? agentDefaults?.customOptions ?? {} - } + maxTurns: + config.maxTurns ?? agentDefaults?.maxTurns ?? DEFAULT_CONFIG.maxTurns, + maxThinkingTokens: + config.maxThinkingTokens ?? + agentDefaults?.maxThinkingTokens ?? + DEFAULT_CONFIG.maxThinkingTokens, + systemPrompt: + config.systemPrompt ?? + agentDefaults?.systemPrompt ?? + DEFAULT_CONFIG.systemPrompt, + mcpServers: + config.mcpServers ?? + agentDefaults?.mcpServers ?? + DEFAULT_CONFIG.mcpServers, + permissionMode: + config.permissionMode ?? + agentDefaults?.permissionMode ?? + DEFAULT_CONFIG.permissionMode, + customOptions: config.customOptions ?? agentDefaults?.customOptions ?? {}, + }; // Initialize metadata this.metadata = { @@ -84,8 +98,8 @@ export abstract class BaseAgent { totalDuration: 0, lastEventTime: Date.now(), toolsExecuted: 0, - state: 'idle' - } + state: 'idle', + }; logger.debug(`🤖 ${agentType} agent created`, { agentType, @@ -93,85 +107,103 @@ export abstract class BaseAgent { maxTurns: this.config.maxTurns, maxThinkingTokens: this.config.maxThinkingTokens, usingDefaultMcp: !config.mcpServers, - usingDefaultPrompt: !config.systemPrompt - }) + usingDefaultPrompt: !config.systemPrompt, + }); + } + + /** + * Async initialization for agents that need it + * Subclasses can override for async setup (e.g., fetching config) + */ + async init(): Promise { + this.initialized = true; } /** * Execute a task and stream events * Must be implemented by concrete agent classes */ - abstract execute(message: string): AsyncGenerator + // FIXME: make it handle init if not initialized + abstract execute(message: string): AsyncGenerator; /** * Cleanup agent resources * Must be implemented by concrete agent classes */ - abstract destroy(): Promise + abstract destroy(): Promise; /** * Get current agent metadata */ getMetadata(): AgentMetadata { - return { ...this.metadata } + return {...this.metadata}; } /** * Helper: Start execution tracking */ protected startExecution(): void { - this.metadata.state = 'executing' - this.executionStartTime = Date.now() + this.metadata.state = 'executing'; + this.executionStartTime = Date.now(); } /** * Helper: Complete execution tracking */ protected completeExecution(): void { - this.metadata.state = 'idle' - this.metadata.totalDuration += Date.now() - this.executionStartTime + this.metadata.state = 'idle'; + this.metadata.totalDuration += Date.now() - this.executionStartTime; } /** * Helper: Mark execution error */ protected errorExecution(error: Error | string): void { - this.metadata.state = 'error' - this.metadata.error = error instanceof Error ? error.message : error + this.metadata.state = 'error'; + this.metadata.error = error instanceof Error ? error.message : error; } /** * Helper: Update last event time */ protected updateEventTime(): void { - this.metadata.lastEventTime = Date.now() + this.metadata.lastEventTime = Date.now(); } /** * Helper: Increment tool execution count */ protected updateToolsExecuted(count: number = 1): void { - this.metadata.toolsExecuted += count + this.metadata.toolsExecuted += count; } /** * Helper: Update turn count */ protected updateTurns(turns: number): void { - this.metadata.turns = turns + this.metadata.turns = turns; } /** * Helper: Check if agent is destroyed */ protected isDestroyed(): boolean { - return this.metadata.state === 'destroyed' + return this.metadata.state === 'destroyed'; } /** * Helper: Mark agent as destroyed */ protected markDestroyed(): void { - this.metadata.state = 'destroyed' + this.metadata.state = 'destroyed'; + } + + /** + * Helper: Ensure agent is initialized + */ + protected ensureInitialized(): void { + if (!this.initialized) { + throw new Error('Agent not initialized. Call init() before execute()'); + } } } diff --git a/packages/agent/src/agent/ClaudeSDKAgent.ts b/packages/agent/src/agent/ClaudeSDKAgent.ts index 02c8653..995f6ec 100644 --- a/packages/agent/src/agent/ClaudeSDKAgent.ts +++ b/packages/agent/src/agent/ClaudeSDKAgent.ts @@ -5,7 +5,7 @@ import { query } from '@anthropic-ai/claude-agent-sdk' import { EventFormatter, FormattedEvent } from '../utils/EventFormatter.js' -import { logger } from '@browseros/common' +import { logger, fetchBrowserOSConfig, type BrowserOSConfig } from '@browseros/common' import type { AgentConfig } from './types.js' import { BaseAgent } from './BaseAgent.js' import { CLAUDE_SDK_SYSTEM_PROMPT } from './ClaudeSDKAgent.prompt.js' @@ -37,6 +37,7 @@ const CLAUDE_SDK_DEFAULTS = { */ export class ClaudeSDKAgent extends BaseAgent { private abortController: AbortController | null = null + private gatewayConfig: BrowserOSConfig | null = null constructor(config: AgentConfig, controllerBridge: ControllerBridge) { logger.info('🔧 Using shared ControllerBridge for controller connection') @@ -60,6 +61,46 @@ export class ClaudeSDKAgent extends BaseAgent { logger.info('✅ ClaudeSDKAgent initialized with shared ControllerBridge') } + /** + * Initialize agent - fetch config from BrowserOS Config URL if configured + * Falls back to ANTHROPIC_API_KEY env var if config URL not set or fails + */ + override async init(): Promise { + const configUrl = process.env.BROWSEROS_CONFIG_URL + + if (configUrl) { + logger.info('🌐 Fetching config from BrowserOS Config URL', { configUrl }) + + try { + this.gatewayConfig = await fetchBrowserOSConfig(configUrl) + this.config.apiKey = this.gatewayConfig.apiKey + + logger.info('✅ Using API key from BrowserOS Config URL', { + model: this.gatewayConfig.model + }) + + await super.init() + return + } catch (error) { + logger.warn('⚠️ Failed to fetch from config URL, falling back to ANTHROPIC_API_KEY', { + error: error instanceof Error ? error.message : String(error) + }) + } + } + + const envApiKey = process.env.ANTHROPIC_API_KEY + if (envApiKey) { + this.config.apiKey = envApiKey + logger.info('✅ Using API key from ANTHROPIC_API_KEY env var') + await super.init() + return + } + + throw new Error( + 'No API key found. Set either BROWSEROS_CONFIG_URL or ANTHROPIC_API_KEY' + ) + } + /** * Execute a task using Claude SDK and stream formatted events * @@ -67,14 +108,16 @@ export class ClaudeSDKAgent extends BaseAgent { * @yields FormattedEvent instances */ async *execute(message: string): AsyncGenerator { - // Start execution tracking + if (!this.initialized) { + await this.init() + } + this.startExecution() this.abortController = new AbortController() logger.info('🤖 ClaudeSDKAgent executing', { message: message.substring(0, 100) }) try { - // Build SDK options with AbortController const options: any = { apiKey: this.config.apiKey, maxTurns: this.config.maxTurns, @@ -86,6 +129,11 @@ export class ClaudeSDKAgent extends BaseAgent { abortController: this.abortController } + if (this.gatewayConfig?.model) { + options.model = this.gatewayConfig.model + logger.debug('Using model from gateway', { model: this.gatewayConfig.model }) + } + // Call Claude SDK const iterator = query({ prompt: message, options })[Symbol.asyncIterator]() diff --git a/packages/common/src/gateway.ts b/packages/common/src/gateway.ts new file mode 100644 index 0000000..964e049 --- /dev/null +++ b/packages/common/src/gateway.ts @@ -0,0 +1,49 @@ +/** + * @license + * Copyright 2025 BrowserOS + */ + +import {logger} from './logger.js'; + +export interface BrowserOSConfig { + model: string; + apiKey: string; +} + +export async function fetchBrowserOSConfig( + configUrl: string, +): Promise { + logger.debug('Fetching BrowserOS config', {configUrl}); + + try { + const response = await fetch(configUrl, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error( + `Failed to fetch config: ${response.status} ${response.statusText} - ${errorText}`, + ); + } + + const config = (await response.json()) as BrowserOSConfig; + + if (!config.model || !config.apiKey) { + throw new Error('Invalid config response: missing model or apiKey'); + } + + logger.info('✅ BrowserOS config fetched'); + + return config; + } catch (error) { + logger.error('❌ Failed to fetch BrowserOS config', { + configUrl, + error: error instanceof Error ? error.message : String(error), + }); + throw error; + } +} diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index e7d9dd1..9cf5812 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -9,6 +9,7 @@ export {McpContext} from './McpContext.js'; export {Mutex} from './Mutex.js'; export {logger} from './logger.js'; export {metrics} from './metrics.js'; +export {fetchBrowserOSConfig} from './gateway.js'; // Utils exports export * from './utils/index.js'; @@ -21,3 +22,4 @@ export type { TextSnapshot, } from './McpContext.js'; export type {TraceResult} from './types.js'; +export type {BrowserOSConfig} from './gateway.js'; diff --git a/packages/server/src/main.ts b/packages/server/src/main.ts index 3f68a09..e745bc7 100644 --- a/packages/server/src/main.ts +++ b/packages/server/src/main.ts @@ -173,16 +173,9 @@ async function startAgentServer( ports: ReturnType, controllerBridge: ControllerBridge, ): Promise { - const apiKey = process.env.ANTHROPIC_API_KEY; - if (!apiKey) { - logger.error('[Agent Server] ANTHROPIC_API_KEY is required'); - logger.error('Please set ANTHROPIC_API_KEY in .env file'); - process.exit(1); - } - const agentConfig: AgentServerConfig = { port: ports.agentPort, - apiKey, + apiKey: process.env.ANTHROPIC_API_KEY || '', cwd: process.cwd(), maxSessions: parseInt(process.env.MAX_SESSIONS || '5'), idleTimeoutMs: parseInt(process.env.SESSION_IDLE_TIMEOUT_MS || '90000'),