From 48218b01a9e625176caa361ee15398627a5765ce Mon Sep 17 00:00:00 2001
From: "tembo[bot]" <208362400+tembo[bot]@users.noreply.github.com>
Date: Fri, 9 Jan 2026 16:13:35 +0000
Subject: [PATCH 1/4] docs: update File I/O examples with improved download and
upload workflows
---
browsers/file-io.mdx | 133 ++++++++++++++++---------------------------
1 file changed, 49 insertions(+), 84 deletions(-)
diff --git a/browsers/file-io.mdx b/browsers/file-io.mdx
index c9691d4..40d8431 100644
--- a/browsers/file-io.mdx
+++ b/browsers/file-io.mdx
@@ -13,10 +13,14 @@ Kernel browsers run in fully sandboxed environments with writable filesystems. W
Playwright performs downloads via the browser itself, so there are a few steps:
- Create a browser session
-- Configure where the browser saves downloads using CDP
+- Configure browser download behavior using CDP
- Perform the download
- Retrieve the file from the browser's filesystem
+
+ With `behavior: 'default'`, downloads are saved to the browser's default download directory at `~/Downloads`. Use Kernel's File I/O APIs to retrieve files from this location.
+
+
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
@@ -30,9 +34,10 @@ Playwright performs downloads via the browser itself, so there are a few steps:
import Kernel from '@onkernel/sdk';
import { chromium } from 'playwright';
import fs from 'fs';
+import os from 'os';
import pTimeout from 'p-timeout';
-const DOWNLOAD_DIR = '/tmp/downloads';
+const DOWNLOAD_DIR = `${os.homedir()}/Downloads`;
const kernel = new Kernel();
// Poll listFiles until the expected file appears in the directory
@@ -63,8 +68,7 @@ async function main() {
const client = await context.newCDPSession(page);
await client.send('Browser.setDownloadBehavior', {
- behavior: 'allow',
- downloadPath: DOWNLOAD_DIR,
+ behavior: 'default',
eventsEnabled: true,
});
@@ -139,11 +143,12 @@ main();
```python Python
import asyncio
import os
+from pathlib import Path
import time
from kernel import Kernel
from playwright.async_api import async_playwright
-DOWNLOAD_DIR = "/tmp/downloads"
+DOWNLOAD_DIR = str(Path.home() / "Downloads")
kernel = Kernel()
@@ -173,8 +178,7 @@ async def main():
await cdp_session.send(
"Browser.setDownloadBehavior",
{
- "behavior": "allow",
- "downloadPath": DOWNLOAD_DIR,
+ "behavior": "default",
"eventsEnabled": True,
},
)
@@ -353,135 +357,96 @@ Browser Use handles downloads automatically when configured properly. Documentat
## Uploads
-You can upload from your local filesystem into the browser directly using Playwright's file input helpers.
+Playwright's `setInputFiles()` method allows you to upload files directly to file input elements. You can provide either a local file path or a buffer with file data.
```typescript Typescript/Javascript
import Kernel from '@onkernel/sdk';
import { chromium } from 'playwright';
-import { config } from 'dotenv';
-
-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';
+import fs from 'fs';
const kernel = new Kernel();
async function main() {
- // 1. Create Kernel browser session
+ // 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
+ // Connect Playwright
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());
+ // Navigate to upload test page
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',
+ // Option 1: Upload from a local file path
+ // await page.locator('#fileUpload').setInputFiles('./path/to/local/file.png');
+
+ // Option 2: Upload from a buffer (useful for dynamically generated content)
+ const fileBuffer = fs.readFileSync('./test-file.txt');
+ await page.locator('#fileUpload').setInputFiles({
+ name: 'test-file.txt',
+ mimeType: 'text/plain',
buffer: fileBuffer,
- }]);
- console.log('Upload completed');
+ });
+ console.log('File selected');
+
+ // Submit the form and verify the upload
+ await page.getByRole('button', { name: 'Upload' }).click();
+ await page.waitForSelector('#uploadResult');
+ const result = await page.locator('#uploadResult').textContent();
+ console.log('Upload result:', result);
await kernel.browsers.deleteByID(kernelBrowser.session_id);
console.log('Browser deleted');
-
- return null;
}
main();
-
````
```python Python
import asyncio
-import os
+from pathlib import Path
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
+ # 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:
+ # Connect 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()
+ # Navigate to upload test 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()
+ # Option 1: Upload from a local file path
+ # await page.locator('#fileUpload').set_input_files('./path/to/local/file.png')
+ # Option 2: Upload from a buffer (useful for dynamically generated content)
+ file_buffer = Path('./test-file.txt').read_bytes()
await page.locator('#fileUpload').set_input_files({
- 'name': FILENAME,
- 'mimeType': 'image/png',
+ 'name': 'test-file.txt',
+ 'mimeType': 'text/plain',
'buffer': file_buffer,
})
- print('Upload completed')
+ print('File selected')
+
+ # Submit the form and verify the upload
+ await page.get_by_role('button', name='Upload').click()
+ await page.wait_for_selector('#uploadResult')
+ result = await page.locator('#uploadResult').text_content()
+ print(f'Upload result: {result}')
await browser.close()
From 26b4ed36a537ebe02ad941dd01c94d553d4f2c17 Mon Sep 17 00:00:00 2001
From: Daniel Prevoznik
Date: Mon, 12 Jan 2026 14:03:07 -0500
Subject: [PATCH 2/4] docs: fix File I/O examples to use CDP filePath for
downloads
Downloads (TypeScript & Python):
- Fix path bug: capture filePath from CDP downloadProgress event instead
of using os.homedir()/Downloads which doesn't work on remote browsers
- Update waitForFile() to accept full file path
- Update Note to explain filePath field in CDP events
Uploads (TypeScript & Python):
- Change selector from #fileUpload to input[type="file"] (more generic)
- Remove non-existent form submission code (#uploadResult, Upload button)
- Update example files from test-file.txt to document.pdf
---
browsers/file-io.mdx | 102 +++++++++++++++++++------------------------
1 file changed, 45 insertions(+), 57 deletions(-)
diff --git a/browsers/file-io.mdx b/browsers/file-io.mdx
index 40d8431..ce342ee 100644
--- a/browsers/file-io.mdx
+++ b/browsers/file-io.mdx
@@ -18,7 +18,7 @@ Playwright performs downloads via the browser itself, so there are a few steps:
- Retrieve the file from the browser's filesystem
- With `behavior: 'default'`, downloads are saved to the browser's default download directory at `~/Downloads`. Use Kernel's File I/O APIs to retrieve files from this location.
+ With `behavior: 'default'`, downloads are saved to the browser's default download directory. The CDP `downloadProgress` event includes a `filePath` field when the download completes, which tells you exactly where the file was saved. Use this path with Kernel's File I/O APIs to retrieve the file.
@@ -34,19 +34,19 @@ Playwright performs downloads via the browser itself, so there are a few steps:
import Kernel from '@onkernel/sdk';
import { chromium } from 'playwright';
import fs from 'fs';
-import os from 'os';
+import path from 'path';
import pTimeout from 'p-timeout';
-const DOWNLOAD_DIR = `${os.homedir()}/Downloads`;
const kernel = new Kernel();
// Poll listFiles until the expected file appears in the directory
async function waitForFile(
sessionId: string,
- dir: string,
- filename: string,
+ filePath: string,
timeoutMs = 30_000
) {
+ const dir = path.dirname(filePath);
+ const filename = path.basename(filePath);
const start = Date.now();
while (Date.now() - start < timeoutMs) {
const files = await kernel.browsers.fs.listFiles(sessionId, { path: dir });
@@ -55,7 +55,7 @@ async function waitForFile(
}
await new Promise((r) => setTimeout(r, 500));
}
- throw new Error(`File ${filename} not found after ${timeoutMs}ms`);
+ throw new Error(`File ${filePath} not found after ${timeoutMs}ms`);
}
async function main() {
@@ -72,8 +72,8 @@ async function main() {
eventsEnabled: true,
});
- // Set up CDP listeners to capture download filename and completion
- let downloadFilename: string | undefined;
+ // Set up CDP listeners to capture download path and completion
+ let downloadFilePath: string | undefined;
let downloadState: string | undefined;
let downloadCompletedResolve!: () => void;
const downloadCompleted = new Promise((resolve) => {
@@ -81,13 +81,13 @@ async function main() {
});
client.on('Browser.downloadWillBegin', (event) => {
- downloadFilename = event.suggestedFilename ?? 'unknown';
- console.log('Download started:', downloadFilename);
+ console.log('Download started:', event.suggestedFilename);
});
client.on('Browser.downloadProgress', (event) => {
if (event.state === 'completed' || event.state === 'canceled') {
downloadState = event.state;
+ downloadFilePath = event.filePath;
downloadCompletedResolve();
}
});
@@ -107,8 +107,8 @@ async function main() {
throw err;
}
- if (!downloadFilename) {
- throw new Error('Unable to determine download filename');
+ if (!downloadFilePath) {
+ throw new Error('Unable to determine download file path');
}
if (downloadState === 'canceled') {
@@ -116,19 +116,18 @@ async function main() {
}
// Wait for the file to be available via Kernel's File I/O APIs
- console.log(`Waiting for file: ${downloadFilename}`);
- await waitForFile(kernelBrowser.session_id, DOWNLOAD_DIR, downloadFilename);
+ console.log(`Waiting for file: ${downloadFilePath}`);
+ await waitForFile(kernelBrowser.session_id, downloadFilePath);
- const remotePath = `${DOWNLOAD_DIR}/${downloadFilename}`;
- console.log(`Reading file: ${remotePath}`);
+ console.log(`Reading file: ${downloadFilePath}`);
const resp = await kernel.browsers.fs.readFile(kernelBrowser.session_id, {
- path: remotePath,
+ path: downloadFilePath,
});
const bytes = await resp.bytes();
fs.mkdirSync('downloads', { recursive: true });
- const localPath = `downloads/${downloadFilename}`;
+ const localPath = `downloads/${path.basename(downloadFilePath)}`;
fs.writeFileSync(localPath, bytes);
console.log(`Saved to ${localPath}`);
@@ -148,21 +147,22 @@ import time
from kernel import Kernel
from playwright.async_api import async_playwright
-DOWNLOAD_DIR = str(Path.home() / "Downloads")
kernel = Kernel()
# Poll list_files until the expected file appears in the directory
async def wait_for_file(
- session_id: str, dir: str, filename: str, timeout_sec: float = 30
+ session_id: str, file_path: str, timeout_sec: float = 30
):
+ dir_path = str(Path(file_path).parent)
+ filename = Path(file_path).name
start = time.time()
while time.time() - start < timeout_sec:
- files = kernel.browsers.fs.list_files(session_id, path=dir)
+ files = kernel.browsers.fs.list_files(session_id, path=dir_path)
if any(f.name == filename for f in files):
return
await asyncio.sleep(0.5)
- raise TimeoutError(f"File {filename} not found after {timeout_sec}s")
+ raise TimeoutError(f"File {file_path} not found after {timeout_sec}s")
async def main():
@@ -184,18 +184,17 @@ async def main():
)
download_completed = asyncio.Event()
- download_filename: str | None = None
+ download_file_path: str | None = None
download_state: str | None = None
def _on_download_begin(event):
- nonlocal download_filename
- download_filename = event.get("suggestedFilename", "unknown")
- print(f"Download started: {download_filename}")
+ print(f"Download started: {event.get('suggestedFilename', 'unknown')}")
def _on_download_progress(event):
- nonlocal download_state
+ nonlocal download_state, download_file_path
if event.get("state") in ["completed", "canceled"]:
download_state = event.get("state")
+ download_file_path = event.get("filePath")
download_completed.set()
cdp_session.on("Browser.downloadWillBegin", _on_download_begin)
@@ -212,17 +211,20 @@ async def main():
print("Download timed out after 10 seconds")
raise
+ if not download_file_path:
+ raise RuntimeError("Unable to determine download file path")
+
if download_state == "canceled":
raise RuntimeError("Download was canceled")
# Wait for the file to be available via Kernel's File I/O APIs
- print(f"Waiting for file: {download_filename}")
- await wait_for_file(kernel_browser.session_id, DOWNLOAD_DIR, download_filename)
+ print(f"Waiting for file: {download_file_path}")
+ await wait_for_file(kernel_browser.session_id, download_file_path)
resp = kernel.browsers.fs.read_file(
- kernel_browser.session_id, path=f"{DOWNLOAD_DIR}/{download_filename}"
+ kernel_browser.session_id, path=download_file_path
)
- local_path = f"./downloads/{download_filename}"
+ local_path = f"./downloads/{Path(download_file_path).name}"
os.makedirs("./downloads", exist_ok=True)
resp.write_to_file(local_path)
print(f"Saved to {local_path}")
@@ -377,28 +379,21 @@ async function main() {
const context = browser.contexts()[0] || (await browser.newContext());
const page = context.pages()[0] || (await context.newPage());
- // Navigate to upload test page
- console.log('Navigating to upload test page');
+ // Navigate to a page with a file input
await page.goto('https://browser-tests-alpha.vercel.app/api/upload-test');
// Option 1: Upload from a local file path
- // await page.locator('#fileUpload').setInputFiles('./path/to/local/file.png');
+ await page.locator('input[type="file"]').setInputFiles('./path/to/local/file.png');
// Option 2: Upload from a buffer (useful for dynamically generated content)
- const fileBuffer = fs.readFileSync('./test-file.txt');
- await page.locator('#fileUpload').setInputFiles({
- name: 'test-file.txt',
- mimeType: 'text/plain',
+ const fileBuffer = fs.readFileSync('./document.pdf');
+ await page.locator('input[type="file"]').setInputFiles({
+ name: 'document.pdf',
+ mimeType: 'application/pdf',
buffer: fileBuffer,
});
console.log('File selected');
- // Submit the form and verify the upload
- await page.getByRole('button', { name: 'Upload' }).click();
- await page.waitForSelector('#uploadResult');
- const result = await page.locator('#uploadResult').textContent();
- console.log('Upload result:', result);
-
await kernel.browsers.deleteByID(kernelBrowser.session_id);
console.log('Browser deleted');
}
@@ -426,28 +421,21 @@ async def main():
context = browser.contexts[0] if browser.contexts else await browser.new_context()
page = context.pages[0] if context.pages else await context.new_page()
- # Navigate to upload test page
- print('Navigating to upload test page')
+ # Navigate to a page with a file input
await page.goto('https://browser-tests-alpha.vercel.app/api/upload-test')
# Option 1: Upload from a local file path
- # await page.locator('#fileUpload').set_input_files('./path/to/local/file.png')
+ await page.locator('input[type="file"]').set_input_files('./path/to/local/file.png')
# Option 2: Upload from a buffer (useful for dynamically generated content)
- file_buffer = Path('./test-file.txt').read_bytes()
- await page.locator('#fileUpload').set_input_files({
- 'name': 'test-file.txt',
- 'mimeType': 'text/plain',
+ file_buffer = Path('./document.pdf').read_bytes()
+ await page.locator('input[type="file"]').set_input_files({
+ 'name': 'document.pdf',
+ 'mimeType': 'application/pdf',
'buffer': file_buffer,
})
print('File selected')
- # Submit the form and verify the upload
- await page.get_by_role('button', name='Upload').click()
- await page.wait_for_selector('#uploadResult')
- result = await page.locator('#uploadResult').text_content()
- print(f'Upload result: {result}')
-
await browser.close()
kernel.browsers.delete_by_id(kernel_browser.session_id)
From 7e20ad8b679d8264c3b205bbb9a997f7ffc09582 Mon Sep 17 00:00:00 2001
From: Daniel Prevoznik
Date: Mon, 12 Jan 2026 14:05:25 -0500
Subject: [PATCH 3/4] Update gitignore
---
.gitignore | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/.gitignore b/.gitignore
index 4f8631f..086e74f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,8 +1,9 @@
*/.DS_Store
.DS_Store
+.env
node_modules/
venv
__pycache__/**
test.py
-./kernel/**
+./kernel/**
\ No newline at end of file
From a8b4bd9a256e9b2d1cc187cb9cabbfcbf56606f2 Mon Sep 17 00:00:00 2001
From: Daniel Prevoznik
Date: Mon, 12 Jan 2026 14:28:13 -0500
Subject: [PATCH 4/4] Update file io uploads examples
Fetch the file and then use set input files to add the buffer values.
---
browsers/file-io.mdx | 48 ++++++++++++++++++++++----------------------
1 file changed, 24 insertions(+), 24 deletions(-)
diff --git a/browsers/file-io.mdx b/browsers/file-io.mdx
index ce342ee..0240b46 100644
--- a/browsers/file-io.mdx
+++ b/browsers/file-io.mdx
@@ -359,14 +359,14 @@ Browser Use handles downloads automatically when configured properly. Documentat
## Uploads
-Playwright's `setInputFiles()` method allows you to upload files directly to file input elements. You can provide either a local file path or a buffer with file data.
+Playwright's `setInputFiles()` method allows you to upload files directly to file input elements. You can fetch a file from a URL and pass the buffer directly to `setInputFiles()`.
```typescript Typescript/Javascript
import Kernel from '@onkernel/sdk';
import { chromium } from 'playwright';
-import fs from 'fs';
+const IMAGE_URL = 'https://www.kernel.sh/brand_assets/Kernel-Logo_Accent.png';
const kernel = new Kernel();
async function main() {
@@ -382,17 +382,16 @@ async function main() {
// Navigate to a page with a file input
await page.goto('https://browser-tests-alpha.vercel.app/api/upload-test');
- // Option 1: Upload from a local file path
- await page.locator('input[type="file"]').setInputFiles('./path/to/local/file.png');
+ // Fetch file and pass buffer directly to setInputFiles
+ const response = await fetch(IMAGE_URL);
+ const buffer = Buffer.from(await response.arrayBuffer());
- // Option 2: Upload from a buffer (useful for dynamically generated content)
- const fileBuffer = fs.readFileSync('./document.pdf');
- await page.locator('input[type="file"]').setInputFiles({
- name: 'document.pdf',
- mimeType: 'application/pdf',
- buffer: fileBuffer,
- });
- console.log('File selected');
+ await page.locator('input[type="file"]').setInputFiles([{
+ name: 'Kernel-Logo_Accent.png',
+ mimeType: 'image/png',
+ buffer: buffer,
+ }]);
+ console.log('File uploaded');
await kernel.browsers.deleteByID(kernelBrowser.session_id);
console.log('Browser deleted');
@@ -403,10 +402,11 @@ main();
```python Python
import asyncio
-from pathlib import Path
+import httpx
from kernel import Kernel
from playwright.async_api import async_playwright
+IMAGE_URL = 'https://www.kernel.sh/brand_assets/Kernel-Logo_Accent.png'
kernel = Kernel()
@@ -424,17 +424,17 @@ async def main():
# Navigate to a page with a file input
await page.goto('https://browser-tests-alpha.vercel.app/api/upload-test')
- # Option 1: Upload from a local file path
- await page.locator('input[type="file"]').set_input_files('./path/to/local/file.png')
-
- # Option 2: Upload from a buffer (useful for dynamically generated content)
- file_buffer = Path('./document.pdf').read_bytes()
- await page.locator('input[type="file"]').set_input_files({
- 'name': 'document.pdf',
- 'mimeType': 'application/pdf',
- 'buffer': file_buffer,
- })
- print('File selected')
+ # Fetch file and pass buffer directly to set_input_files
+ async with httpx.AsyncClient() as client:
+ response = await client.get(IMAGE_URL)
+ buffer = response.content
+
+ await page.locator('input[type="file"]').set_input_files([{
+ 'name': 'Kernel-Logo_Accent.png',
+ 'mimeType': 'image/png',
+ 'buffer': buffer,
+ }])
+ print('File uploaded')
await browser.close()