feat: add Kernel as cloud browser provider (#200)
Add Kernel (https://kernel.sh) as a third-party cloud browser provider, following the same pattern as Browserbase and Browser Use integrations. Features: - Launch browser with `-p kernel` flag or `AGENT_BROWSER_PROVIDER=kernel` - Configurable via environment variables: - KERNEL_API_KEY (required) - KERNEL_HEADLESS (default: false) - KERNEL_STEALTH (default: true) - KERNEL_TIMEOUT_SECONDS (default: 300) - KERNEL_PROFILE_NAME (optional, for persistent sessions) - Profile find-or-create: automatically creates profile if it doesn't exist - Profile persistence: cookies/logins saved back to profile on session close - Uses raw fetch() calls for API communication (no SDK dependency) Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
a99f59cd20
commit
fcee8f70d1
+172
@@ -74,6 +74,8 @@ export class BrowserManager {
|
||||
private browserbaseApiKey: string | null = null;
|
||||
private browserUseSessionId: string | null = null;
|
||||
private browserUseApiKey: string | null = null;
|
||||
private kernelSessionId: string | null = null;
|
||||
private kernelApiKey: string | null = null;
|
||||
private contexts: BrowserContext[] = [];
|
||||
private pages: Page[] = [];
|
||||
private activePageIndex: number = 0;
|
||||
@@ -710,6 +712,22 @@ export class BrowserManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close a Kernel session via API
|
||||
*/
|
||||
private async closeKernelSession(sessionId: string, apiKey: string): Promise<void> {
|
||||
const response = await fetch(`https://api.onkernel.com/browsers/${sessionId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to close Kernel session: ${response.statusText}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to Browserbase remote browser via CDP.
|
||||
* Requires BROWSERBASE_API_KEY and BROWSERBASE_PROJECT_ID environment variables.
|
||||
@@ -771,6 +789,147 @@ export class BrowserManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find or create a Kernel profile by name.
|
||||
* Returns the profile object if successful.
|
||||
*/
|
||||
private async findOrCreateKernelProfile(
|
||||
profileName: string,
|
||||
apiKey: string
|
||||
): Promise<{ name: string }> {
|
||||
// First, try to get the existing profile
|
||||
const getResponse = await fetch(
|
||||
`https://api.onkernel.com/profiles/${encodeURIComponent(profileName)}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (getResponse.ok) {
|
||||
// Profile exists, return it
|
||||
return { name: profileName };
|
||||
}
|
||||
|
||||
if (getResponse.status !== 404) {
|
||||
throw new Error(`Failed to check Kernel profile: ${getResponse.statusText}`);
|
||||
}
|
||||
|
||||
// Profile doesn't exist, create it
|
||||
const createResponse = await fetch('https://api.onkernel.com/profiles', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
body: JSON.stringify({ name: profileName }),
|
||||
});
|
||||
|
||||
if (!createResponse.ok) {
|
||||
throw new Error(`Failed to create Kernel profile: ${createResponse.statusText}`);
|
||||
}
|
||||
|
||||
return { name: profileName };
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to Kernel remote browser via CDP.
|
||||
* Requires KERNEL_API_KEY environment variable.
|
||||
*/
|
||||
private async connectToKernel(): Promise<void> {
|
||||
const kernelApiKey = process.env.KERNEL_API_KEY;
|
||||
if (!kernelApiKey) {
|
||||
throw new Error('KERNEL_API_KEY is required when using kernel as a provider');
|
||||
}
|
||||
|
||||
// Find or create profile if KERNEL_PROFILE_NAME is set
|
||||
const profileName = process.env.KERNEL_PROFILE_NAME;
|
||||
let profileConfig: { profile: { name: string; save_changes: boolean } } | undefined;
|
||||
|
||||
if (profileName) {
|
||||
await this.findOrCreateKernelProfile(profileName, kernelApiKey);
|
||||
profileConfig = {
|
||||
profile: {
|
||||
name: profileName,
|
||||
save_changes: true, // Save cookies/state back to the profile when session ends
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const response = await fetch('https://api.onkernel.com/browsers', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${kernelApiKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
// Kernel browsers are headful by default with stealth mode available
|
||||
// The user can configure these via environment variables if needed
|
||||
headless: process.env.KERNEL_HEADLESS?.toLowerCase() === 'true',
|
||||
stealth: process.env.KERNEL_STEALTH?.toLowerCase() !== 'false', // Default to stealth mode
|
||||
timeout_seconds: parseInt(process.env.KERNEL_TIMEOUT_SECONDS || '300', 10),
|
||||
// Load and save to a profile if specified
|
||||
...profileConfig,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to create Kernel session: ${response.statusText}`);
|
||||
}
|
||||
|
||||
let session: { session_id: string; cdp_ws_url: string };
|
||||
try {
|
||||
session = (await response.json()) as { session_id: string; cdp_ws_url: string };
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to parse Kernel session response: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!session.session_id || !session.cdp_ws_url) {
|
||||
throw new Error(
|
||||
`Invalid Kernel session response: missing ${!session.session_id ? 'session_id' : 'cdp_ws_url'}`
|
||||
);
|
||||
}
|
||||
|
||||
const browser = await chromium.connectOverCDP(session.cdp_ws_url).catch(() => {
|
||||
throw new Error('Failed to connect to Kernel session via CDP');
|
||||
});
|
||||
|
||||
try {
|
||||
const contexts = browser.contexts();
|
||||
let context: BrowserContext;
|
||||
let page: Page;
|
||||
|
||||
// Kernel browsers launch with a default context and page
|
||||
if (contexts.length === 0) {
|
||||
context = await browser.newContext();
|
||||
page = await context.newPage();
|
||||
} else {
|
||||
context = contexts[0];
|
||||
const pages = context.pages();
|
||||
page = pages[0] ?? (await context.newPage());
|
||||
}
|
||||
|
||||
this.kernelSessionId = session.session_id;
|
||||
this.kernelApiKey = kernelApiKey;
|
||||
this.browser = browser;
|
||||
context.setDefaultTimeout(60000);
|
||||
this.contexts.push(context);
|
||||
this.pages.push(page);
|
||||
this.activePageIndex = 0;
|
||||
this.setupPageTracking(page);
|
||||
this.setupContextTracking(context);
|
||||
} catch (error) {
|
||||
await this.closeKernelSession(session.session_id, kernelApiKey).catch((sessionError) => {
|
||||
console.error('Failed to close Kernel session during cleanup:', sessionError);
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to Browser Use remote browser via CDP.
|
||||
* Requires BROWSER_USE_API_KEY environment variable.
|
||||
@@ -903,6 +1062,12 @@ export class BrowserManager {
|
||||
return;
|
||||
}
|
||||
|
||||
// Kernel: requires explicit opt-in via -p kernel flag or AGENT_BROWSER_PROVIDER=kernel
|
||||
if (provider === 'kernel') {
|
||||
await this.connectToKernel();
|
||||
return;
|
||||
}
|
||||
|
||||
const browserType = options.browser ?? 'chromium';
|
||||
if (hasExtensions && browserType !== 'chromium') {
|
||||
throw new Error('Extensions are only supported in Chromium');
|
||||
@@ -1653,6 +1818,11 @@ export class BrowserManager {
|
||||
}
|
||||
);
|
||||
this.browser = null;
|
||||
} else if (this.kernelSessionId && this.kernelApiKey) {
|
||||
await this.closeKernelSession(this.kernelSessionId, this.kernelApiKey).catch((error) => {
|
||||
console.error('Failed to close Kernel session:', error);
|
||||
});
|
||||
this.browser = null;
|
||||
} else if (this.cdpEndpoint !== null) {
|
||||
// CDP: only disconnect, don't close external app's pages
|
||||
if (this.browser) {
|
||||
@@ -1680,6 +1850,8 @@ export class BrowserManager {
|
||||
this.browserbaseApiKey = null;
|
||||
this.browserUseSessionId = null;
|
||||
this.browserUseApiKey = null;
|
||||
this.kernelSessionId = null;
|
||||
this.kernelApiKey = null;
|
||||
this.isPersistentContext = false;
|
||||
this.activePageIndex = 0;
|
||||
this.refMap = {};
|
||||
|
||||
Reference in New Issue
Block a user