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:
Rafael
2026-01-26 00:22:25 -06:00
committed by GitHub
co-authored by Claude Opus 4.5
parent a99f59cd20
commit fcee8f70d1
2 changed files with 206 additions and 0 deletions
+172
View File
@@ -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 = {};