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
+34
View File
@@ -739,6 +739,40 @@ When enabled, agent-browser connects to a Browser Use cloud session instead of l
Get your API key from the [Browser Use Cloud Dashboard](https://cloud.browser-use.com/settings?tab=api-keys). Free credits are available to get started, with pay-as-you-go pricing after.
### Kernel
[Kernel](https://www.kernel.sh) provides cloud browser infrastructure for AI agents with features like stealth mode and persistent profiles.
To enable Kernel, use the `-p` flag:
```bash
export KERNEL_API_KEY="your-api-key"
agent-browser -p kernel open https://example.com
```
Or use environment variables for CI/scripts:
```bash
export AGENT_BROWSER_PROVIDER=kernel
export KERNEL_API_KEY="your-api-key"
agent-browser open https://example.com
```
Optional configuration via environment variables:
| Variable | Description | Default |
|----------|-------------|---------|
| `KERNEL_HEADLESS` | Run browser in headless mode (`true`/`false`) | `false` |
| `KERNEL_STEALTH` | Enable stealth mode to avoid bot detection (`true`/`false`) | `true` |
| `KERNEL_TIMEOUT_SECONDS` | Session timeout in seconds | `300` |
| `KERNEL_PROFILE_NAME` | Browser profile name for persistent cookies/logins (created if it doesn't exist) | (none) |
When enabled, agent-browser connects to a Kernel cloud session instead of launching a local browser. All commands work identically.
**Profile Persistence:** When `KERNEL_PROFILE_NAME` is set, the profile will be created if it doesn't already exist. Cookies, logins, and session data are automatically saved back to the profile when the browser session ends, making them available for future sessions.
Get your API key from the [Kernel Dashboard](https://dashboard.onkernel.com).
## License
Apache-2.0
+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 = {};