Skip to content

Commit 2b3cb86

Browse files
authored
get API key from browseros config (#19)
1 parent 007aa91 commit 2b3cb86

File tree

6 files changed

+171
-47
lines changed

6 files changed

+171
-47
lines changed

.env.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Required
1+
BROWSEROS_CONFIG_URL=
22
ANTHROPIC_API_KEY=
33

44
# Server Ports

packages/agent/src/agent/BaseAgent.ts

Lines changed: 67 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,16 @@
33
* Copyright 2025 BrowserOS
44
*/
55

6-
import { logger } from '@browseros/common'
7-
import type { AgentConfig, AgentMetadata } from './types.js'
8-
import type { FormattedEvent } from '../utils/EventFormatter.js'
6+
import {logger} from '@browseros/common';
7+
import type {AgentConfig, AgentMetadata} from './types.js';
8+
import type {FormattedEvent} from '../utils/EventFormatter.js';
99

1010
/**
1111
* Generic default system prompt for agents
1212
*
1313
* Minimal prompt - agents should override with their own specific prompts
1414
*/
15-
export const DEFAULT_SYSTEM_PROMPT = `You are a browser automation agent.`
15+
export const DEFAULT_SYSTEM_PROMPT = `You are a browser automation agent.`;
1616

1717
/**
1818
* Generic default configuration values
@@ -24,8 +24,8 @@ export const DEFAULT_CONFIG = {
2424
maxThinkingTokens: 10000,
2525
systemPrompt: DEFAULT_SYSTEM_PROMPT,
2626
mcpServers: {},
27-
permissionMode: 'bypassPermissions' as const
28-
}
27+
permissionMode: 'bypassPermissions' as const,
28+
};
2929

3030
/**
3131
* BaseAgent - Abstract base class for all agent implementations
@@ -56,26 +56,40 @@ export const DEFAULT_CONFIG = {
5656
* }
5757
*/
5858
export abstract class BaseAgent {
59-
protected config: Required<AgentConfig>
60-
protected metadata: AgentMetadata
61-
protected executionStartTime: number = 0
59+
protected config: Required<AgentConfig>;
60+
protected metadata: AgentMetadata;
61+
protected executionStartTime: number = 0;
62+
protected initialized: boolean = false;
6263

6364
constructor(
6465
agentType: string,
6566
config: AgentConfig,
66-
agentDefaults?: Partial<AgentConfig>
67+
agentDefaults?: Partial<AgentConfig>,
6768
) {
6869
// Merge config with agent-specific defaults, then with base defaults
6970
this.config = {
7071
apiKey: config.apiKey,
7172
cwd: config.cwd,
72-
maxTurns: config.maxTurns ?? agentDefaults?.maxTurns ?? DEFAULT_CONFIG.maxTurns,
73-
maxThinkingTokens: config.maxThinkingTokens ?? agentDefaults?.maxThinkingTokens ?? DEFAULT_CONFIG.maxThinkingTokens,
74-
systemPrompt: config.systemPrompt ?? agentDefaults?.systemPrompt ?? DEFAULT_CONFIG.systemPrompt,
75-
mcpServers: config.mcpServers ?? agentDefaults?.mcpServers ?? DEFAULT_CONFIG.mcpServers,
76-
permissionMode: config.permissionMode ?? agentDefaults?.permissionMode ?? DEFAULT_CONFIG.permissionMode,
77-
customOptions: config.customOptions ?? agentDefaults?.customOptions ?? {}
78-
}
73+
maxTurns:
74+
config.maxTurns ?? agentDefaults?.maxTurns ?? DEFAULT_CONFIG.maxTurns,
75+
maxThinkingTokens:
76+
config.maxThinkingTokens ??
77+
agentDefaults?.maxThinkingTokens ??
78+
DEFAULT_CONFIG.maxThinkingTokens,
79+
systemPrompt:
80+
config.systemPrompt ??
81+
agentDefaults?.systemPrompt ??
82+
DEFAULT_CONFIG.systemPrompt,
83+
mcpServers:
84+
config.mcpServers ??
85+
agentDefaults?.mcpServers ??
86+
DEFAULT_CONFIG.mcpServers,
87+
permissionMode:
88+
config.permissionMode ??
89+
agentDefaults?.permissionMode ??
90+
DEFAULT_CONFIG.permissionMode,
91+
customOptions: config.customOptions ?? agentDefaults?.customOptions ?? {},
92+
};
7993

8094
// Initialize metadata
8195
this.metadata = {
@@ -84,94 +98,112 @@ export abstract class BaseAgent {
8498
totalDuration: 0,
8599
lastEventTime: Date.now(),
86100
toolsExecuted: 0,
87-
state: 'idle'
88-
}
101+
state: 'idle',
102+
};
89103

90104
logger.debug(`🤖 ${agentType} agent created`, {
91105
agentType,
92106
cwd: this.config.cwd,
93107
maxTurns: this.config.maxTurns,
94108
maxThinkingTokens: this.config.maxThinkingTokens,
95109
usingDefaultMcp: !config.mcpServers,
96-
usingDefaultPrompt: !config.systemPrompt
97-
})
110+
usingDefaultPrompt: !config.systemPrompt,
111+
});
112+
}
113+
114+
/**
115+
* Async initialization for agents that need it
116+
* Subclasses can override for async setup (e.g., fetching config)
117+
*/
118+
async init(): Promise<void> {
119+
this.initialized = true;
98120
}
99121

100122
/**
101123
* Execute a task and stream events
102124
* Must be implemented by concrete agent classes
103125
*/
104-
abstract execute(message: string): AsyncGenerator<FormattedEvent>
126+
// FIXME: make it handle init if not initialized
127+
abstract execute(message: string): AsyncGenerator<FormattedEvent>;
105128

106129
/**
107130
* Cleanup agent resources
108131
* Must be implemented by concrete agent classes
109132
*/
110-
abstract destroy(): Promise<void>
133+
abstract destroy(): Promise<void>;
111134

112135
/**
113136
* Get current agent metadata
114137
*/
115138
getMetadata(): AgentMetadata {
116-
return { ...this.metadata }
139+
return {...this.metadata};
117140
}
118141

119142
/**
120143
* Helper: Start execution tracking
121144
*/
122145
protected startExecution(): void {
123-
this.metadata.state = 'executing'
124-
this.executionStartTime = Date.now()
146+
this.metadata.state = 'executing';
147+
this.executionStartTime = Date.now();
125148
}
126149

127150
/**
128151
* Helper: Complete execution tracking
129152
*/
130153
protected completeExecution(): void {
131-
this.metadata.state = 'idle'
132-
this.metadata.totalDuration += Date.now() - this.executionStartTime
154+
this.metadata.state = 'idle';
155+
this.metadata.totalDuration += Date.now() - this.executionStartTime;
133156
}
134157

135158
/**
136159
* Helper: Mark execution error
137160
*/
138161
protected errorExecution(error: Error | string): void {
139-
this.metadata.state = 'error'
140-
this.metadata.error = error instanceof Error ? error.message : error
162+
this.metadata.state = 'error';
163+
this.metadata.error = error instanceof Error ? error.message : error;
141164
}
142165

143166
/**
144167
* Helper: Update last event time
145168
*/
146169
protected updateEventTime(): void {
147-
this.metadata.lastEventTime = Date.now()
170+
this.metadata.lastEventTime = Date.now();
148171
}
149172

150173
/**
151174
* Helper: Increment tool execution count
152175
*/
153176
protected updateToolsExecuted(count: number = 1): void {
154-
this.metadata.toolsExecuted += count
177+
this.metadata.toolsExecuted += count;
155178
}
156179

157180
/**
158181
* Helper: Update turn count
159182
*/
160183
protected updateTurns(turns: number): void {
161-
this.metadata.turns = turns
184+
this.metadata.turns = turns;
162185
}
163186

164187
/**
165188
* Helper: Check if agent is destroyed
166189
*/
167190
protected isDestroyed(): boolean {
168-
return this.metadata.state === 'destroyed'
191+
return this.metadata.state === 'destroyed';
169192
}
170193

171194
/**
172195
* Helper: Mark agent as destroyed
173196
*/
174197
protected markDestroyed(): void {
175-
this.metadata.state = 'destroyed'
198+
this.metadata.state = 'destroyed';
199+
}
200+
201+
/**
202+
* Helper: Ensure agent is initialized
203+
*/
204+
protected ensureInitialized(): void {
205+
if (!this.initialized) {
206+
throw new Error('Agent not initialized. Call init() before execute()');
207+
}
176208
}
177209
}

packages/agent/src/agent/ClaudeSDKAgent.ts

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import { query } from '@anthropic-ai/claude-agent-sdk'
77
import { EventFormatter, FormattedEvent } from '../utils/EventFormatter.js'
8-
import { logger } from '@browseros/common'
8+
import { logger, fetchBrowserOSConfig, type BrowserOSConfig } from '@browseros/common'
99
import type { AgentConfig } from './types.js'
1010
import { BaseAgent } from './BaseAgent.js'
1111
import { CLAUDE_SDK_SYSTEM_PROMPT } from './ClaudeSDKAgent.prompt.js'
@@ -37,6 +37,7 @@ const CLAUDE_SDK_DEFAULTS = {
3737
*/
3838
export class ClaudeSDKAgent extends BaseAgent {
3939
private abortController: AbortController | null = null
40+
private gatewayConfig: BrowserOSConfig | null = null
4041

4142
constructor(config: AgentConfig, controllerBridge: ControllerBridge) {
4243
logger.info('🔧 Using shared ControllerBridge for controller connection')
@@ -60,21 +61,63 @@ export class ClaudeSDKAgent extends BaseAgent {
6061
logger.info('✅ ClaudeSDKAgent initialized with shared ControllerBridge')
6162
}
6263

64+
/**
65+
* Initialize agent - fetch config from BrowserOS Config URL if configured
66+
* Falls back to ANTHROPIC_API_KEY env var if config URL not set or fails
67+
*/
68+
override async init(): Promise<void> {
69+
const configUrl = process.env.BROWSEROS_CONFIG_URL
70+
71+
if (configUrl) {
72+
logger.info('🌐 Fetching config from BrowserOS Config URL', { configUrl })
73+
74+
try {
75+
this.gatewayConfig = await fetchBrowserOSConfig(configUrl)
76+
this.config.apiKey = this.gatewayConfig.apiKey
77+
78+
logger.info('✅ Using API key from BrowserOS Config URL', {
79+
model: this.gatewayConfig.model
80+
})
81+
82+
await super.init()
83+
return
84+
} catch (error) {
85+
logger.warn('⚠️ Failed to fetch from config URL, falling back to ANTHROPIC_API_KEY', {
86+
error: error instanceof Error ? error.message : String(error)
87+
})
88+
}
89+
}
90+
91+
const envApiKey = process.env.ANTHROPIC_API_KEY
92+
if (envApiKey) {
93+
this.config.apiKey = envApiKey
94+
logger.info('✅ Using API key from ANTHROPIC_API_KEY env var')
95+
await super.init()
96+
return
97+
}
98+
99+
throw new Error(
100+
'No API key found. Set either BROWSEROS_CONFIG_URL or ANTHROPIC_API_KEY'
101+
)
102+
}
103+
63104
/**
64105
* Execute a task using Claude SDK and stream formatted events
65106
*
66107
* @param message - User's natural language request
67108
* @yields FormattedEvent instances
68109
*/
69110
async *execute(message: string): AsyncGenerator<FormattedEvent> {
70-
// Start execution tracking
111+
if (!this.initialized) {
112+
await this.init()
113+
}
114+
71115
this.startExecution()
72116
this.abortController = new AbortController()
73117

74118
logger.info('🤖 ClaudeSDKAgent executing', { message: message.substring(0, 100) })
75119

76120
try {
77-
// Build SDK options with AbortController
78121
const options: any = {
79122
apiKey: this.config.apiKey,
80123
maxTurns: this.config.maxTurns,
@@ -86,6 +129,11 @@ export class ClaudeSDKAgent extends BaseAgent {
86129
abortController: this.abortController
87130
}
88131

132+
if (this.gatewayConfig?.model) {
133+
options.model = this.gatewayConfig.model
134+
logger.debug('Using model from gateway', { model: this.gatewayConfig.model })
135+
}
136+
89137
// Call Claude SDK
90138
const iterator = query({ prompt: message, options })[Symbol.asyncIterator]()
91139

packages/common/src/gateway.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* @license
3+
* Copyright 2025 BrowserOS
4+
*/
5+
6+
import {logger} from './logger.js';
7+
8+
export interface BrowserOSConfig {
9+
model: string;
10+
apiKey: string;
11+
}
12+
13+
export async function fetchBrowserOSConfig(
14+
configUrl: string,
15+
): Promise<BrowserOSConfig> {
16+
logger.debug('Fetching BrowserOS config', {configUrl});
17+
18+
try {
19+
const response = await fetch(configUrl, {
20+
method: 'GET',
21+
headers: {
22+
'Content-Type': 'application/json',
23+
},
24+
});
25+
26+
if (!response.ok) {
27+
const errorText = await response.text();
28+
throw new Error(
29+
`Failed to fetch config: ${response.status} ${response.statusText} - ${errorText}`,
30+
);
31+
}
32+
33+
const config = (await response.json()) as BrowserOSConfig;
34+
35+
if (!config.model || !config.apiKey) {
36+
throw new Error('Invalid config response: missing model or apiKey');
37+
}
38+
39+
logger.info('✅ BrowserOS config fetched');
40+
41+
return config;
42+
} catch (error) {
43+
logger.error('❌ Failed to fetch BrowserOS config', {
44+
configUrl,
45+
error: error instanceof Error ? error.message : String(error),
46+
});
47+
throw error;
48+
}
49+
}

packages/common/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export {McpContext} from './McpContext.js';
99
export {Mutex} from './Mutex.js';
1010
export {logger} from './logger.js';
1111
export {metrics} from './metrics.js';
12+
export {fetchBrowserOSConfig} from './gateway.js';
1213

1314
// Utils exports
1415
export * from './utils/index.js';
@@ -21,3 +22,4 @@ export type {
2122
TextSnapshot,
2223
} from './McpContext.js';
2324
export type {TraceResult} from './types.js';
25+
export type {BrowserOSConfig} from './gateway.js';

packages/server/src/main.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -173,16 +173,9 @@ async function startAgentServer(
173173
ports: ReturnType<typeof parseArguments>,
174174
controllerBridge: ControllerBridge,
175175
): Promise<any> {
176-
const apiKey = process.env.ANTHROPIC_API_KEY;
177-
if (!apiKey) {
178-
logger.error('[Agent Server] ANTHROPIC_API_KEY is required');
179-
logger.error('Please set ANTHROPIC_API_KEY in .env file');
180-
process.exit(1);
181-
}
182-
183176
const agentConfig: AgentServerConfig = {
184177
port: ports.agentPort,
185-
apiKey,
178+
apiKey: process.env.ANTHROPIC_API_KEY || '',
186179
cwd: process.cwd(),
187180
maxSessions: parseInt(process.env.MAX_SESSIONS || '5'),
188181
idleTimeoutMs: parseInt(process.env.SESSION_IDLE_TIMEOUT_MS || '90000'),

0 commit comments

Comments
 (0)