Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 172 additions & 0 deletions src/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -676,6 +678,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.
Expand Down Expand Up @@ -737,6 +755,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.
Expand Down Expand Up @@ -856,6 +1015,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');
Expand Down Expand Up @@ -1604,6 +1769,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) {
Expand Down Expand Up @@ -1631,6 +1801,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 = {};
Expand Down