Skip to content

Commit fcee8f7

Browse files
rgarciaclaude
andauthored
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>
1 parent a99f59c commit fcee8f7

2 files changed

Lines changed: 206 additions & 0 deletions

File tree

README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -739,6 +739,40 @@ When enabled, agent-browser connects to a Browser Use cloud session instead of l
739739
740740
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.
741741
742+
### Kernel
743+
744+
[Kernel](https://www.kernel.sh) provides cloud browser infrastructure for AI agents with features like stealth mode and persistent profiles.
745+
746+
To enable Kernel, use the `-p` flag:
747+
748+
```bash
749+
export KERNEL_API_KEY="your-api-key"
750+
agent-browser -p kernel open https://example.com
751+
```
752+
753+
Or use environment variables for CI/scripts:
754+
755+
```bash
756+
export AGENT_BROWSER_PROVIDER=kernel
757+
export KERNEL_API_KEY="your-api-key"
758+
agent-browser open https://example.com
759+
```
760+
761+
Optional configuration via environment variables:
762+
763+
| Variable | Description | Default |
764+
|----------|-------------|---------|
765+
| `KERNEL_HEADLESS` | Run browser in headless mode (`true`/`false`) | `false` |
766+
| `KERNEL_STEALTH` | Enable stealth mode to avoid bot detection (`true`/`false`) | `true` |
767+
| `KERNEL_TIMEOUT_SECONDS` | Session timeout in seconds | `300` |
768+
| `KERNEL_PROFILE_NAME` | Browser profile name for persistent cookies/logins (created if it doesn't exist) | (none) |
769+
770+
When enabled, agent-browser connects to a Kernel cloud session instead of launching a local browser. All commands work identically.
771+
772+
**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.
773+
774+
Get your API key from the [Kernel Dashboard](https://dashboard.onkernel.com).
775+
742776
## License
743777
744778
Apache-2.0

src/browser.ts

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ export class BrowserManager {
7474
private browserbaseApiKey: string | null = null;
7575
private browserUseSessionId: string | null = null;
7676
private browserUseApiKey: string | null = null;
77+
private kernelSessionId: string | null = null;
78+
private kernelApiKey: string | null = null;
7779
private contexts: BrowserContext[] = [];
7880
private pages: Page[] = [];
7981
private activePageIndex: number = 0;
@@ -710,6 +712,22 @@ export class BrowserManager {
710712
}
711713
}
712714

715+
/**
716+
* Close a Kernel session via API
717+
*/
718+
private async closeKernelSession(sessionId: string, apiKey: string): Promise<void> {
719+
const response = await fetch(`https://api.onkernel.com/browsers/${sessionId}`, {
720+
method: 'DELETE',
721+
headers: {
722+
Authorization: `Bearer ${apiKey}`,
723+
},
724+
});
725+
726+
if (!response.ok) {
727+
throw new Error(`Failed to close Kernel session: ${response.statusText}`);
728+
}
729+
}
730+
713731
/**
714732
* Connect to Browserbase remote browser via CDP.
715733
* Requires BROWSERBASE_API_KEY and BROWSERBASE_PROJECT_ID environment variables.
@@ -771,6 +789,147 @@ export class BrowserManager {
771789
}
772790
}
773791

792+
/**
793+
* Find or create a Kernel profile by name.
794+
* Returns the profile object if successful.
795+
*/
796+
private async findOrCreateKernelProfile(
797+
profileName: string,
798+
apiKey: string
799+
): Promise<{ name: string }> {
800+
// First, try to get the existing profile
801+
const getResponse = await fetch(
802+
`https://api.onkernel.com/profiles/${encodeURIComponent(profileName)}`,
803+
{
804+
method: 'GET',
805+
headers: {
806+
Authorization: `Bearer ${apiKey}`,
807+
},
808+
}
809+
);
810+
811+
if (getResponse.ok) {
812+
// Profile exists, return it
813+
return { name: profileName };
814+
}
815+
816+
if (getResponse.status !== 404) {
817+
throw new Error(`Failed to check Kernel profile: ${getResponse.statusText}`);
818+
}
819+
820+
// Profile doesn't exist, create it
821+
const createResponse = await fetch('https://api.onkernel.com/profiles', {
822+
method: 'POST',
823+
headers: {
824+
'Content-Type': 'application/json',
825+
Authorization: `Bearer ${apiKey}`,
826+
},
827+
body: JSON.stringify({ name: profileName }),
828+
});
829+
830+
if (!createResponse.ok) {
831+
throw new Error(`Failed to create Kernel profile: ${createResponse.statusText}`);
832+
}
833+
834+
return { name: profileName };
835+
}
836+
837+
/**
838+
* Connect to Kernel remote browser via CDP.
839+
* Requires KERNEL_API_KEY environment variable.
840+
*/
841+
private async connectToKernel(): Promise<void> {
842+
const kernelApiKey = process.env.KERNEL_API_KEY;
843+
if (!kernelApiKey) {
844+
throw new Error('KERNEL_API_KEY is required when using kernel as a provider');
845+
}
846+
847+
// Find or create profile if KERNEL_PROFILE_NAME is set
848+
const profileName = process.env.KERNEL_PROFILE_NAME;
849+
let profileConfig: { profile: { name: string; save_changes: boolean } } | undefined;
850+
851+
if (profileName) {
852+
await this.findOrCreateKernelProfile(profileName, kernelApiKey);
853+
profileConfig = {
854+
profile: {
855+
name: profileName,
856+
save_changes: true, // Save cookies/state back to the profile when session ends
857+
},
858+
};
859+
}
860+
861+
const response = await fetch('https://api.onkernel.com/browsers', {
862+
method: 'POST',
863+
headers: {
864+
'Content-Type': 'application/json',
865+
Authorization: `Bearer ${kernelApiKey}`,
866+
},
867+
body: JSON.stringify({
868+
// Kernel browsers are headful by default with stealth mode available
869+
// The user can configure these via environment variables if needed
870+
headless: process.env.KERNEL_HEADLESS?.toLowerCase() === 'true',
871+
stealth: process.env.KERNEL_STEALTH?.toLowerCase() !== 'false', // Default to stealth mode
872+
timeout_seconds: parseInt(process.env.KERNEL_TIMEOUT_SECONDS || '300', 10),
873+
// Load and save to a profile if specified
874+
...profileConfig,
875+
}),
876+
});
877+
878+
if (!response.ok) {
879+
throw new Error(`Failed to create Kernel session: ${response.statusText}`);
880+
}
881+
882+
let session: { session_id: string; cdp_ws_url: string };
883+
try {
884+
session = (await response.json()) as { session_id: string; cdp_ws_url: string };
885+
} catch (error) {
886+
throw new Error(
887+
`Failed to parse Kernel session response: ${error instanceof Error ? error.message : String(error)}`
888+
);
889+
}
890+
891+
if (!session.session_id || !session.cdp_ws_url) {
892+
throw new Error(
893+
`Invalid Kernel session response: missing ${!session.session_id ? 'session_id' : 'cdp_ws_url'}`
894+
);
895+
}
896+
897+
const browser = await chromium.connectOverCDP(session.cdp_ws_url).catch(() => {
898+
throw new Error('Failed to connect to Kernel session via CDP');
899+
});
900+
901+
try {
902+
const contexts = browser.contexts();
903+
let context: BrowserContext;
904+
let page: Page;
905+
906+
// Kernel browsers launch with a default context and page
907+
if (contexts.length === 0) {
908+
context = await browser.newContext();
909+
page = await context.newPage();
910+
} else {
911+
context = contexts[0];
912+
const pages = context.pages();
913+
page = pages[0] ?? (await context.newPage());
914+
}
915+
916+
this.kernelSessionId = session.session_id;
917+
this.kernelApiKey = kernelApiKey;
918+
this.browser = browser;
919+
context.setDefaultTimeout(60000);
920+
this.contexts.push(context);
921+
this.pages.push(page);
922+
this.activePageIndex = 0;
923+
this.setupPageTracking(page);
924+
this.setupContextTracking(context);
925+
} catch (error) {
926+
await this.closeKernelSession(session.session_id, kernelApiKey).catch((sessionError) => {
927+
console.error('Failed to close Kernel session during cleanup:', sessionError);
928+
});
929+
throw error;
930+
}
931+
}
932+
774933
/**
775934
* Connect to Browser Use remote browser via CDP.
776935
* Requires BROWSER_USE_API_KEY environment variable.
@@ -903,6 +1062,12 @@ export class BrowserManager {
9031062
return;
9041063
}
9051064

1065+
// Kernel: requires explicit opt-in via -p kernel flag or AGENT_BROWSER_PROVIDER=kernel
1066+
if (provider === 'kernel') {
1067+
await this.connectToKernel();
1068+
return;
1069+
}
1070+
9061071
const browserType = options.browser ?? 'chromium';
9071072
if (hasExtensions && browserType !== 'chromium') {
9081073
throw new Error('Extensions are only supported in Chromium');
@@ -1653,6 +1818,11 @@ export class BrowserManager {
16531818
}
16541819
);
16551820
this.browser = null;
1821+
} else if (this.kernelSessionId && this.kernelApiKey) {
1822+
await this.closeKernelSession(this.kernelSessionId, this.kernelApiKey).catch((error) => {
1823+
console.error('Failed to close Kernel session:', error);
1824+
});
1825+
this.browser = null;
16561826
} else if (this.cdpEndpoint !== null) {
16571827
// CDP: only disconnect, don't close external app's pages
16581828
if (this.browser) {
@@ -1680,6 +1850,8 @@ export class BrowserManager {
16801850
this.browserbaseApiKey = null;
16811851
this.browserUseSessionId = null;
16821852
this.browserUseApiKey = null;
1853+
this.kernelSessionId = null;
1854+
this.kernelApiKey = null;
16831855
this.isPersistentContext = false;
16841856
this.activePageIndex = 0;
16851857
this.refMap = {};

0 commit comments

Comments
 (0)