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()