diff --git a/browsers/file-io.mdx b/browsers/file-io.mdx
index c1cbacd..c9691d4 100644
--- a/browsers/file-io.mdx
+++ b/browsers/file-io.mdx
@@ -3,19 +3,26 @@ title: "File I/O"
description: "Downloads, uploads, and manipulating the browser's filesystem"
---
-Kernel browsers run in fully sandboxed environments with a writable filesystem that you control. Anything your automation downloads during a session is saved inside this filesystem and can be retrieved directly while the session is running.
-
## Downloads
+Kernel browsers run in fully sandboxed environments with writable filesystems. When your automation downloads a file, it's saved inside the browser's filesystem and can be retrieved using Kernel's File I/O APIs.
+
+
+### Playwright
+
Playwright performs downloads via the browser itself, so there are a few steps:
- Create a browser session
-- Configure where the browser saves downloads
+- Configure where the browser saves downloads using CDP
- Perform the download
- Retrieve the file from the browser's filesystem
-The CDP `downloadProgress` event signals when the browser finishes writing a file, but there may be a brief delay before the file becomes available through Kernel's File I/O APIs. This is especially true for larger downloads. We recommend polling `listFiles` to confirm the file exists before attempting to read it.
+ The CDP `downloadProgress` event signals when the browser finishes writing a
+ file, but there may be a brief delay before the file becomes available through
+ Kernel's File I/O APIs. This is especially true for larger downloads. We
+ recommend polling `listFiles` to confirm the file exists before attempting to
+ read it.
@@ -126,7 +133,8 @@ async function main() {
}
main();
-```
+
+````
```python Python
import asyncio
@@ -221,10 +229,127 @@ async def main():
if __name__ == "__main__":
asyncio.run(main())
-```
+````
+
-We recommend using the [list files](/api-reference/browsers/list-files-in-a-directory) API to poll for file availability before calling [read file](/api-reference/browsers/read-file-contents), as shown in the examples above. This approach ensures reliable downloads, especially for larger files. You can also use `listFiles` to enumerate and save all downloads at the end of a session.
+We recommend using the [list files](/api-reference/browsers/list-files-in-a-directory) API to poll for file availability before calling [read file](/api-reference/browsers/read-file-contents), as shown in the examples above. This approach ensures reliable downloads, especially for larger files. You can also use `listFiles` to enumerate and save all downloads at the end of a session.
+
+### Stagehand v3
+
+When using Stagehand with Kernel browsers, you need to configure the download behavior in the `localBrowserLaunchOptions`:
+
+```typescript
+const stagehand = new Stagehand({
+ env: "LOCAL",
+ verbose: 1,
+ localBrowserLaunchOptions: {
+ cdpUrl: kernelBrowser.cdp_ws_url,
+ downloadsPath: DOWNLOAD_DIR, // Specify where downloads should be saved
+ acceptDownloads: true, // Enable downloads
+ },
+});
+```
+
+Here's a complete example:
+
+```typescript
+import { Stagehand } from "@browserbasehq/stagehand";
+import Kernel from "@onkernel/sdk";
+import fs from "fs";
+
+const DOWNLOAD_DIR = "/tmp/downloads";
+
+// Poll listFiles until any file appears in the directory
+async function waitForFile(
+ kernel: Kernel,
+ sessionId: string,
+ dir: string,
+ timeoutMs = 30_000
+) {
+ const start = Date.now();
+ while (Date.now() - start < timeoutMs) {
+ const files = await kernel.browsers.fs.listFiles(sessionId, { path: dir });
+ if (files.length > 0) {
+ return files[0];
+ }
+ await new Promise((r) => setTimeout(r, 500));
+ }
+ throw new Error(`No files found in ${dir} after ${timeoutMs}ms`);
+}
+
+async function main() {
+ const kernel = new Kernel();
+
+ console.log("Creating browser via Kernel...");
+ const kernelBrowser = await kernel.browsers.create({
+ stealth: true,
+ });
+
+ console.log(`Kernel Browser Session Started`);
+ console.log(`Session ID: ${kernelBrowser.session_id}`);
+ console.log(`Watch live: ${kernelBrowser.browser_live_view_url}`);
+
+ // Initialize Stagehand with Kernel's CDP URL and download configuration
+ const stagehand = new Stagehand({
+ env: "LOCAL",
+ verbose: 1,
+ localBrowserLaunchOptions: {
+ cdpUrl: kernelBrowser.cdp_ws_url,
+ downloadsPath: DOWNLOAD_DIR,
+ acceptDownloads: true,
+ },
+ });
+
+ await stagehand.init();
+
+ const page = stagehand.context.pages()[0];
+
+ await page.goto("https://browser-tests-alpha.vercel.app/api/download-test");
+
+ // Use Stagehand to click the download button
+ await stagehand.act("Click the download file link");
+ console.log("Download triggered");
+
+ // Wait for the file to be fully available via Kernel's File I/O APIs
+ console.log("Waiting for file to appear...");
+ const downloadedFile = await waitForFile(
+ kernel,
+ kernelBrowser.session_id,
+ DOWNLOAD_DIR
+ );
+ console.log(`File found: ${downloadedFile.name}`);
+
+ const remotePath = `${DOWNLOAD_DIR}/${downloadedFile.name}`;
+ console.log(`Reading file from: ${remotePath}`);
+
+ // Read the file from Kernel browser's filesystem
+ const resp = await kernel.browsers.fs.readFile(kernelBrowser.session_id, {
+ path: remotePath,
+ });
+
+ // Save to local filesystem
+ const bytes = await resp.bytes();
+ fs.mkdirSync("downloads", { recursive: true });
+ const localPath = `downloads/${downloadedFile.name}`;
+ fs.writeFileSync(localPath, bytes);
+ console.log(`Saved to ${localPath}`);
+
+ // Clean up
+ await stagehand.close();
+ await kernel.browsers.deleteByID(kernelBrowser.session_id);
+ console.log("Browser session closed");
+}
+
+main().catch((err) => {
+ console.error(err);
+ process.exit(1);
+});
+```
+
+### Browser Use
+
+Browser Use handles downloads automatically when configured properly. Documentation for Browser Use downloads coming soon.
## Uploads
@@ -232,18 +357,140 @@ You can upload from your local filesystem into the browser directly using Playwr
```typescript Typescript/Javascript
-const localPath = '/path/to/a/file.txt';
+import Kernel from '@onkernel/sdk';
+import { chromium } from 'playwright';
+import { config } from 'dotenv';
-console.log(`Uploading ${localPath}...`);
-await page.locator('#fileUpload').setInputFiles(localPath);
-console.log('Upload completed');
-```
+config();
+
+const REMOTE_DIR = '/tmp/downloads';
+const FILENAME = 'Kernel-Logo_Accent.png';
+const IMAGE_URL = 'https://www.onkernel.com/brand_assets/Kernel-Logo_Accent.png';
+
+const kernel = new Kernel();
+
+async function main() {
+ // 1. Create Kernel browser session
+ const kernelBrowser = await kernel.browsers.create();
+ console.log('Live view:', kernelBrowser.browser_live_view_url);
+
+ // 2. Fetch the image from URL
+ console.log(`Fetching image from ${IMAGE_URL}`);
+ const response = await fetch(IMAGE_URL);
+ if (!response.ok) {
+ throw new Error(`Failed to fetch image: ${response.status}`);
+ }
+ const imageBlob = await response.blob();
+
+ // 3. Write the fetched image to the remote browser's filesystem
+ const remotePath = `${REMOTE_DIR}/${FILENAME}`;
+ console.log(`Writing to remote browser at ${remotePath}`);
+ await kernel.browsers.fs.writeFile(kernelBrowser.session_id, imageBlob, {
+ path: remotePath,
+ });
+ console.log('File written to remote browser');
+
+ // 4. Connect Playwright and navigate to upload test page
+ const browser = await chromium.connectOverCDP(kernelBrowser.cdp_ws_url);
+ const context = browser.contexts()[0] || (await browser.newContext());
+ const page = context.pages()[0] || (await context.newPage());
+
+ console.log('Navigating to upload test page');
+ await page.goto('https://browser-tests-alpha.vercel.app/api/upload-test');
+
+ // 5. Upload the file using Playwright's file input helper
+ console.log(`Uploading ${remotePath} via file input`);
+ const remoteFile = await kernel.browsers.fs.readFile(kernelBrowser.session_id, { path: remotePath });
+ const fileBuffer = Buffer.from(await remoteFile.bytes());
+ await page.locator('#fileUpload').setInputFiles([{
+ name: FILENAME,
+ mimeType: 'image/png',
+ buffer: fileBuffer,
+ }]);
+ console.log('Upload completed');
+
+ await kernel.browsers.deleteByID(kernelBrowser.session_id);
+ console.log('Browser deleted');
+
+ return null;
+}
+
+main();
+
+````
```python Python
-local_path = "/path/to/a/file.txt"
+import asyncio
+import os
+from kernel import Kernel
+from playwright.async_api import async_playwright
+from dotenv import load_dotenv
+
+load_dotenv()
+
+REMOTE_DIR = '/tmp/downloads'
+FILENAME = 'Kernel-Logo_Accent.png'
+IMAGE_URL = 'https://www.onkernel.com/brand_assets/Kernel-Logo_Accent.png'
+
+kernel = Kernel()
+
+
+async def main():
+ # 1. Create Kernel browser session
+ kernel_browser = kernel.browsers.create()
+ print(f'Live view: {kernel_browser.browser_live_view_url}')
+
+ # 2. Fetch the image from URL
+ print(f'Fetching image from {IMAGE_URL}')
+ import aiohttp
+ async with aiohttp.ClientSession() as session:
+ async with session.get(IMAGE_URL) as response:
+ if response.status != 200:
+ raise Exception(f'Failed to fetch image: {response.status}')
+ image_bytes = await response.read()
+
+ # 3. Write the fetched image to the remote browser's filesystem
+ remote_path = f'{REMOTE_DIR}/{FILENAME}'
+ print(f'Writing to remote browser at {remote_path}')
+ kernel.browsers.fs.write_file(
+ kernel_browser.session_id,
+ image_bytes,
+ path=remote_path
+ )
+ print('File written to remote browser')
+
+ # 4. Connect Playwright and navigate to upload test page
+ async with async_playwright() as playwright:
+ browser = await playwright.chromium.connect_over_cdp(kernel_browser.cdp_ws_url)
+ context = browser.contexts[0] if browser.contexts else await browser.new_context()
+ page = context.pages[0] if context.pages else await context.new_page()
+
+ print('Navigating to upload test page')
+ await page.goto('https://browser-tests-alpha.vercel.app/api/upload-test')
+
+ # 5. Upload the file using Playwright's file input helper
+ print(f'Uploading {remote_path} via file input')
+ remote_file = kernel.browsers.fs.read_file(
+ kernel_browser.session_id,
+ path=remote_path
+ )
+ file_buffer = remote_file.read()
+
+ await page.locator('#fileUpload').set_input_files({
+ 'name': FILENAME,
+ 'mimeType': 'image/png',
+ 'buffer': file_buffer,
+ })
+ print('Upload completed')
+
+ await browser.close()
+
+ kernel.browsers.delete_by_id(kernel_browser.session_id)
+ print('Browser deleted')
+
+
+if __name__ == '__main__':
+ asyncio.run(main())
+````
-print(f"Uploading {local_path}...")
-await page.locator("#fileUpload").set_input_files(str(local_path))
-print("Upload completed")
-```