# Create an API key Source: https://kernel.sh/docs/api-reference/api-keys/create-an-api-key https://api.onkernel.com/spec.json post /org/api_keys Create a new API key within the authenticated organization. # Delete an API key Source: https://kernel.sh/docs/api-reference/api-keys/delete-an-api-key https://api.onkernel.com/spec.json delete /org/api_keys/{id} Delete an API key. A key cannot delete itself; use a different key to delete this one. # Get an API key Source: https://kernel.sh/docs/api-reference/api-keys/get-an-api-key https://api.onkernel.com/spec.json get /org/api_keys/{id} Retrieve an API key by ID for the authenticated organization. API keys are masked. # List API keys Source: https://kernel.sh/docs/api-reference/api-keys/list-api-keys https://api.onkernel.com/spec.json get /org/api_keys List API keys for the authenticated organization. API keys are masked. # Rotate an API key Source: https://kernel.sh/docs/api-reference/api-keys/rotate-an-api-key https://api.onkernel.com/spec.json post /org/api_keys/{id}/rotate Rotate an API key. Issues a new key that copies the name and project of the rotated key, and schedules the rotated key to expire after a grace period so in-flight callers can swap over. The new plaintext key is returned once. # Update an API key Source: https://kernel.sh/docs/api-reference/api-keys/update-an-api-key https://api.onkernel.com/spec.json patch /org/api_keys/{id} Update an API key's name. # List apps Source: https://kernel.sh/docs/api-reference/apps/list-apps https://api.onkernel.com/spec.json get /apps List applications. Optionally filter by app name and/or version label. # Create an audit log export destination Source: https://kernel.sh/docs/api-reference/audit-logs/create-an-audit-log-export-destination https://api.onkernel.com/spec.json post /audit-logs/export/destinations Create a paused destination. Activate it with a status update once the destination test passes. Requires an active Enterprise plan. # Delete an audit log export destination Source: https://kernel.sh/docs/api-reference/audit-logs/delete-an-audit-log-export-destination https://api.onkernel.com/spec.json delete /audit-logs/export/destinations/{id} Soft delete the destination and prevent new delivery attempts. An S3 upload already in progress may complete after the response. # Download an audit-log export chunk Source: https://kernel.sh/docs/api-reference/audit-logs/download-an-audit-log-export-chunk https://api.onkernel.com/spec.json get /audit-logs/export/chunk Download an organization's audit log records for a time range as a file, for archival, compliance, or offline analysis. For interactive browsing, use GET /audit-logs. # List audit log export destinations Source: https://kernel.sh/docs/api-reference/audit-logs/list-audit-log-export-destinations https://api.onkernel.com/spec.json get /audit-logs/export/destinations List audit log export destinations for the organization with pagination support. # List audit logs Source: https://kernel.sh/docs/api-reference/audit-logs/list-audit-logs https://api.onkernel.com/spec.json get /audit-logs API for searching audit logs. Limited to at most 30 day search, returns up to 100 records per page. Not recommended for bulk export. # Retrieve an audit log export destination Source: https://kernel.sh/docs/api-reference/audit-logs/retrieve-an-audit-log-export-destination https://api.onkernel.com/spec.json get /audit-logs/export/destinations/{id} Retrieve details for a single audit log export destination by its ID. # Test an audit log export destination Source: https://kernel.sh/docs/api-reference/audit-logs/test-an-audit-log-export-destination https://api.onkernel.com/spec.json post /audit-logs/export/destinations/{id}/test Verify the destination is writable by assuming the configured role and uploading a temporary probe object with the same request metadata as a real delivery. Requires an active Enterprise plan. # Update an audit log export destination Source: https://kernel.sh/docs/api-reference/audit-logs/update-an-audit-log-export-destination https://api.onkernel.com/spec.json patch /audit-logs/export/destinations/{id} Apply a partial update to a destination. Requires an active Enterprise plan. Returns 409 when the destination was changed concurrently, because the merged configuration this request validated is no longer the one that would be stored; retry against fresh state. Pausing prevents new delivery attempts, but an S3 upload already in progress may complete after the response. # Get authentication context Source: https://kernel.sh/docs/api-reference/authentication/get-authentication-context https://api.onkernel.com/spec.json get /auth/context Returns the authenticated principal, organization, credential scope, and effective request scope. The response is derived from the verified request context and does not expose credential secrets. # Capture a screenshot of the browser instance Source: https://kernel.sh/docs/api-reference/browser-computer-controls/capture-a-screenshot-of-the-browser-instance https://api.onkernel.com/spec.json post /browsers/{id_or_name}/computer/screenshot # Drag the mouse along a path Source: https://kernel.sh/docs/api-reference/browser-computer-controls/drag-the-mouse-along-a-path https://api.onkernel.com/spec.json post /browsers/{id_or_name}/computer/drag_mouse # Execute a batch of computer actions sequentially Source: https://kernel.sh/docs/api-reference/browser-computer-controls/execute-a-batch-of-computer-actions-sequentially https://api.onkernel.com/spec.json post /browsers/{id_or_name}/computer/batch Send an array of computer actions to execute in order on the browser instance. Execution stops on the first error. This reduces network latency compared to sending individual action requests. # Get the current mouse cursor position on the browser instance Source: https://kernel.sh/docs/api-reference/browser-computer-controls/get-the-current-mouse-cursor-position-on-the-browser-instance https://api.onkernel.com/spec.json post /browsers/{id_or_name}/computer/get_mouse_position # Move the mouse cursor to the specified coordinates on the browser instance Source: https://kernel.sh/docs/api-reference/browser-computer-controls/move-the-mouse-cursor-to-the-specified-coordinates-on-the-browser-instance https://api.onkernel.com/spec.json post /browsers/{id_or_name}/computer/move_mouse # Press one or more keys on the host computer Source: https://kernel.sh/docs/api-reference/browser-computer-controls/press-one-or-more-keys-on-the-host-computer https://api.onkernel.com/spec.json post /browsers/{id_or_name}/computer/press_key # Read text from the clipboard on the browser instance Source: https://kernel.sh/docs/api-reference/browser-computer-controls/read-text-from-the-clipboard-on-the-browser-instance https://api.onkernel.com/spec.json post /browsers/{id_or_name}/computer/clipboard/read # Scroll the mouse wheel at a position on the host computer Source: https://kernel.sh/docs/api-reference/browser-computer-controls/scroll-the-mouse-wheel-at-a-position-on-the-host-computer https://api.onkernel.com/spec.json post /browsers/{id_or_name}/computer/scroll # Set cursor visibility Source: https://kernel.sh/docs/api-reference/browser-computer-controls/set-cursor-visibility https://api.onkernel.com/spec.json post /browsers/{id_or_name}/computer/cursor # Simulate a mouse click action on the browser instance Source: https://kernel.sh/docs/api-reference/browser-computer-controls/simulate-a-mouse-click-action-on-the-browser-instance https://api.onkernel.com/spec.json post /browsers/{id_or_name}/computer/click_mouse # Type text on the browser instance Source: https://kernel.sh/docs/api-reference/browser-computer-controls/type-text-on-the-browser-instance https://api.onkernel.com/spec.json post /browsers/{id_or_name}/computer/type # Write text to the clipboard on the browser instance Source: https://kernel.sh/docs/api-reference/browser-computer-controls/write-text-to-the-clipboard-on-the-browser-instance https://api.onkernel.com/spec.json post /browsers/{id_or_name}/computer/clipboard/write # Create a new directory Source: https://kernel.sh/docs/api-reference/browser-filesystem/create-a-new-directory https://api.onkernel.com/spec.json put /browsers/{id_or_name}/fs/create_directory # Delete a directory Source: https://kernel.sh/docs/api-reference/browser-filesystem/delete-a-directory https://api.onkernel.com/spec.json put /browsers/{id_or_name}/fs/delete_directory # Delete a file Source: https://kernel.sh/docs/api-reference/browser-filesystem/delete-a-file https://api.onkernel.com/spec.json put /browsers/{id_or_name}/fs/delete_file # Download a directory as a ZIP archive Source: https://kernel.sh/docs/api-reference/browser-filesystem/download-a-directory-as-a-zip-archive https://api.onkernel.com/spec.json get /browsers/{id_or_name}/fs/download_dir_zip Returns a ZIP file containing the contents of the specified directory. # Get information about a file or directory Source: https://kernel.sh/docs/api-reference/browser-filesystem/get-information-about-a-file-or-directory https://api.onkernel.com/spec.json get /browsers/{id_or_name}/fs/file_info # List files in a directory Source: https://kernel.sh/docs/api-reference/browser-filesystem/list-files-in-a-directory https://api.onkernel.com/spec.json get /browsers/{id_or_name}/fs/list_files # Move or rename a file or directory Source: https://kernel.sh/docs/api-reference/browser-filesystem/move-or-rename-a-file-or-directory https://api.onkernel.com/spec.json put /browsers/{id_or_name}/fs/move # Read file contents Source: https://kernel.sh/docs/api-reference/browser-filesystem/read-file-contents https://api.onkernel.com/spec.json get /browsers/{id_or_name}/fs/read_file # Set file or directory permissions/ownership Source: https://kernel.sh/docs/api-reference/browser-filesystem/set-file-or-directory-permissionsownership https://api.onkernel.com/spec.json put /browsers/{id_or_name}/fs/set_file_permissions # Stop watching a directory Source: https://kernel.sh/docs/api-reference/browser-filesystem/stop-watching-a-directory https://api.onkernel.com/spec.json delete /browsers/{id_or_name}/fs/watch/{watch_id} # Stream filesystem events for a watch Source: https://kernel.sh/docs/api-reference/browser-filesystem/stream-filesystem-events-for-a-watch https://api.onkernel.com/spec.json get /browsers/{id_or_name}/fs/watch/{watch_id}/events # Upload a zip archive and extract it Source: https://kernel.sh/docs/api-reference/browser-filesystem/upload-a-zip-archive-and-extract-it https://api.onkernel.com/spec.json post /browsers/{id_or_name}/fs/upload_zip Upload a zip file and extract its contents to the specified destination path. # Upload one or more files Source: https://kernel.sh/docs/api-reference/browser-filesystem/upload-one-or-more-files https://api.onkernel.com/spec.json post /browsers/{id_or_name}/fs/upload Allows uploading single or multiple files to the remote filesystem. # Watch a directory for changes Source: https://kernel.sh/docs/api-reference/browser-filesystem/watch-a-directory-for-changes https://api.onkernel.com/spec.json post /browsers/{id_or_name}/fs/watch # Write or create a file Source: https://kernel.sh/docs/api-reference/browser-filesystem/write-or-create-a-file https://api.onkernel.com/spec.json put /browsers/{id_or_name}/fs/write_file # Stream log files on the browser instance via SSE Source: https://kernel.sh/docs/api-reference/browser-logs/stream-log-files-on-the-browser-instance-via-sse https://api.onkernel.com/spec.json get /browsers/{id_or_name}/logs/stream # Execute Playwright/TypeScript code against the browser Source: https://kernel.sh/docs/api-reference/browser-playwright/execute-playwrighttypescript-code-against-the-browser https://api.onkernel.com/spec.json post /browsers/{id_or_name}/playwright/execute Execute arbitrary Playwright code in a fresh execution context against the browser. The code runs in the same VM as the browser, minimizing latency and maximizing throughput. It has access to 'page', 'context', 'browser', and 'webmcp' variables. Use 'webmcp.listTools()' to discover browser-wide WebMCP tools and 'webmcp.invokeTool(toolRef, input?, { timeoutSec? })' to invoke an exact registration. It can `return` a value, and this value is returned in the response. # Acquire a browser from the pool Source: https://kernel.sh/docs/api-reference/browser-pools/acquire-a-browser-from-the-pool https://api.onkernel.com/spec.json post /browser_pools/{id_or_name}/acquire Long-polling endpoint to acquire a browser from the pool. Returns immediately when a browser is available, or returns 204 No Content when the poll times out. The client should retry the request to continue waiting for a browser. The acquired browser will use the pool's timeout_seconds for its idle timeout. # Create a browser pool Source: https://kernel.sh/docs/api-reference/browser-pools/create-a-browser-pool https://api.onkernel.com/spec.json post /browser_pools Create a new browser pool with the specified configuration and size. Pooled browsers load their profile read-only: any save_changes on the profile is ignored (not rejected), so pooled browsers never persist changes back to the profile. # Delete a browser pool Source: https://kernel.sh/docs/api-reference/browser-pools/delete-a-browser-pool https://api.onkernel.com/spec.json delete /browser_pools/{id_or_name} Delete a browser pool and all browsers in it. By default, deletion is blocked if browsers are currently leased. Use force=true to terminate leased browsers. # Flush all idle browsers in the pool Source: https://kernel.sh/docs/api-reference/browser-pools/flush-all-idle-browsers-in-the-pool https://api.onkernel.com/spec.json post /browser_pools/{id_or_name}/flush Destroys all idle browsers in the pool; leased browsers are not affected. # Get browser pool details Source: https://kernel.sh/docs/api-reference/browser-pools/get-browser-pool-details https://api.onkernel.com/spec.json get /browser_pools/{id_or_name} Retrieve details for a single browser pool by its ID or name. # List browser pools Source: https://kernel.sh/docs/api-reference/browser-pools/list-browser-pools https://api.onkernel.com/spec.json get /browser_pools List browser pools in the resolved project. # Release a browser back to the pool Source: https://kernel.sh/docs/api-reference/browser-pools/release-a-browser-back-to-the-pool https://api.onkernel.com/spec.json post /browser_pools/{id_or_name}/release Release a browser back to the pool, optionally recreating the browser instance. # Update a browser pool Source: https://kernel.sh/docs/api-reference/browser-pools/update-a-browser-pool https://api.onkernel.com/spec.json patch /browser_pools/{id_or_name} Updates the configuration used to create browsers in the pool. As with creation, save_changes on the pool profile is ignored (not rejected); pooled browsers never persist changes back to the profile. To clear the profile reference, send `profile: { "id": "" }`. Clearing the profile also disables `refresh_on_profile_update`. # Execute a command asynchronously Source: https://kernel.sh/docs/api-reference/browser-processes/execute-a-command-asynchronously https://api.onkernel.com/spec.json post /browsers/{id_or_name}/process/spawn # Execute a command synchronously Source: https://kernel.sh/docs/api-reference/browser-processes/execute-a-command-synchronously https://api.onkernel.com/spec.json post /browsers/{id_or_name}/process/exec # Get process status Source: https://kernel.sh/docs/api-reference/browser-processes/get-process-status https://api.onkernel.com/spec.json get /browsers/{id_or_name}/process/{process_id}/status # Resize a PTY-backed process terminal Source: https://kernel.sh/docs/api-reference/browser-processes/resize-a-pty-backed-process-terminal https://api.onkernel.com/spec.json post /browsers/{id_or_name}/process/{process_id}/resize # Send signal to process Source: https://kernel.sh/docs/api-reference/browser-processes/send-signal-to-process https://api.onkernel.com/spec.json post /browsers/{id_or_name}/process/{process_id}/kill # Stream process stdout via SSE Source: https://kernel.sh/docs/api-reference/browser-processes/stream-process-stdout-via-sse https://api.onkernel.com/spec.json get /browsers/{id_or_name}/process/{process_id}/stdout/stream # Write to process stdin Source: https://kernel.sh/docs/api-reference/browser-processes/write-to-process-stdin https://api.onkernel.com/spec.json post /browsers/{id_or_name}/process/{process_id}/stdin # Download a replay recording Source: https://kernel.sh/docs/api-reference/browser-replays/download-a-replay-recording https://api.onkernel.com/spec.json get /browsers/{id_or_name}/replays/{replay_id} Download or stream the specified replay recording. # List browser session replays Source: https://kernel.sh/docs/api-reference/browser-replays/list-browser-session-replays https://api.onkernel.com/spec.json get /browsers/{id_or_name}/replays List all replays for the specified browser session. # Start a browser session replay recording Source: https://kernel.sh/docs/api-reference/browser-replays/start-a-browser-session-replay-recording https://api.onkernel.com/spec.json post /browsers/{id_or_name}/replays Start recording the browser session and return a replay ID. # Stop a browser session replay recording Source: https://kernel.sh/docs/api-reference/browser-replays/stop-a-browser-session-replay-recording https://api.onkernel.com/spec.json post /browsers/{id_or_name}/replays/{replay_id}/stop Stop the specified replay recording and persist the video. # Create an OTLP destination Source: https://kernel.sh/docs/api-reference/browser-telemetry/create-an-otlp-destination https://api.onkernel.com/spec.json post /telemetry/destinations Create an OTLP export destination in the authenticated organization. Names must be unique within the organization. Requires an organization-scoped credential or dashboard authentication; project-scoped credentials receive a 403. # Delete OTLP destination by ID or name Source: https://kernel.sh/docs/api-reference/browser-telemetry/delete-otlp-destination-by-id-or-name https://api.onkernel.com/spec.json delete /telemetry/destinations/{id_or_name} Delete an OTLP destination. Sessions bound to it are still exporting, so the delete is refused with a 409 while any exist; either wait for those sessions to end or delete them first. It is refused the same way while a managed auth connection still selects it, because that connection re-resolves the destination on every login, and while a managed auth login using it is still in progress. Requires an organization-scoped credential or dashboard authentication; project-scoped credentials receive a 403. # Get OTLP destination by ID or name Source: https://kernel.sh/docs/api-reference/browser-telemetry/get-otlp-destination-by-id-or-name https://api.onkernel.com/spec.json get /telemetry/destinations/{id_or_name} Retrieve a customer-visible OTLP destination in the authenticated organization by its ID or name. Project-scoped credentials can retrieve these destinations for selection by workloads in their project. Non-dashboard reads return header values redacted. # List OTLP destinations Source: https://kernel.sh/docs/api-reference/browser-telemetry/list-otlp-destinations https://api.onkernel.com/spec.json get /telemetry/destinations List customer-visible OTLP export destinations in the authenticated organization. Project-scoped credentials can list these destinations for selection by workloads in their project. Non-dashboard reads return header values redacted. # Read telemetry events for a browser session Source: https://kernel.sh/docs/api-reference/browser-telemetry/read-telemetry-events-for-a-browser-session https://api.onkernel.com/spec.json get /browsers/{id_or_name}/telemetry/events Reads a page of telemetry events for the browser session. To page through results, pass the X-Next-Offset value from the previous response as offset and repeat while X-Has-More is true. Returns an empty list when telemetry data is unavailable. # Stream telemetry events via SSE Source: https://kernel.sh/docs/api-reference/browser-telemetry/stream-telemetry-events-via-sse https://api.onkernel.com/spec.json get /browsers/{id_or_name}/telemetry/stream Streams browser telemetry events as a server-sent events (SSE) stream. The stream closes when the browser session terminates. Each event frame includes an id: field containing a monotonically increasing sequence number; pass it as Last-Event-ID on reconnect to resume without gaps. The event: field is never set; all frames carry JSON in the data: field. A keepalive comment frame is sent every 15 seconds when no events arrive. Returns 404 if the browser session does not exist. If telemetry was not enabled on the session, the stream opens but no events are delivered. Fresh connections only see new events; pass replay=all to start from the oldest retained event instead. # Update OTLP destination by ID or name Source: https://kernel.sh/docs/api-reference/browser-telemetry/update-otlp-destination-by-id-or-name https://api.onkernel.com/spec.json patch /telemetry/destinations/{id_or_name} Update an OTLP destination. Sessions already exporting to it pick up the new values without restarting, which makes this the way to rotate credentials without interrupting export. Names must be unique within the organization. Renaming is refused with a 409 while a managed auth connection selects this destination by name, since that connection resolves the name on every login. Every other field, including `headers`, stays editable. Requires an organization-scoped credential or dashboard authentication; project-scoped credentials receive a 403. # Discover WebMCP tools across the browser Source: https://kernel.sh/docs/api-reference/browser-webmcp/discover-webmcp-tools-across-the-browser https://api.onkernel.com/spec.json get /browsers/{id_or_name}/webmcp/tools Returns a snapshot of native WebMCP tools available across every open tab and embedded frame in the browser. Each tool includes an opaque tool_ref for invoking that exact live registration. Tools disappear when their document closes or navigates away. # Invoke a discovered WebMCP tool Source: https://kernel.sh/docs/api-reference/browser-webmcp/invoke-a-discovered-webmcp-tool https://api.onkernel.com/spec.json post /browsers/{id_or_name}/webmcp/invoke Invokes the exact live registration identified by tool_ref and waits synchronously for its result. Navigation during execution is allowed. If the tab or embedded frame disappears, or the request times out after invocation begins, the response reports outcome_unknown and the tool is not retried. # Ad-hoc upload one or more unpacked extensions to a running browser instance. Source: https://kernel.sh/docs/api-reference/browsers/ad-hoc-upload-one-or-more-unpacked-extensions-to-a-running-browser-instance https://api.onkernel.com/spec.json post /browsers/{id_or_name}/extensions Loads one or more unpacked extensions using live CDP activation when eligible. Chromium restarts when enterprise policy requires it or live activation fails. # Create a browser session Source: https://kernel.sh/docs/api-reference/browsers/create-a-browser-session https://api.onkernel.com/spec.json post /browsers Create a new browser session from within an action. # Delete a browser session by ID or name. Source: https://kernel.sh/docs/api-reference/browsers/delete-a-browser-session-by-id-or-name https://api.onkernel.com/spec.json delete /browsers/{id_or_name} Delete a browser session by ID or name # Get browser session details Source: https://kernel.sh/docs/api-reference/browsers/get-browser-session-details https://api.onkernel.com/spec.json get /browsers/{id_or_name} Get information about a browser session. # List browser sessions Source: https://kernel.sh/docs/api-reference/browsers/list-browser-sessions https://api.onkernel.com/spec.json get /browsers List all browser sessions with pagination support. Use status parameter to filter by session state. # Make an HTTP request through the browser's network stack Source: https://kernel.sh/docs/api-reference/browsers/make-an-http-request-through-the-browsers-network-stack https://api.onkernel.com/spec.json post /browsers/{id_or_name}/curl Sends an HTTP request through Chrome's HTTP request stack, inheriting the browser's TLS fingerprint, cookies, proxy configuration, and headers. Returns a structured JSON response with status, headers, body, and timing. # Update browser session Source: https://kernel.sh/docs/api-reference/browsers/update-browser-session https://api.onkernel.com/spec.json patch /browsers/{id_or_name} Update a browser session. # Create a deployment Source: https://kernel.sh/docs/api-reference/deployments/create-a-deployment https://api.onkernel.com/spec.json post /deployments Create a new deployment. # Delete a deployment Source: https://kernel.sh/docs/api-reference/deployments/delete-a-deployment https://api.onkernel.com/spec.json delete /deployments/{id} Stops a running deployment and marks it for deletion. If the deployment is already in a terminal state (stopped or failed), returns immediately. # Get deployment details Source: https://kernel.sh/docs/api-reference/deployments/get-deployment-details https://api.onkernel.com/spec.json get /deployments/{id} Get information about a deployment's status. # List deployments Source: https://kernel.sh/docs/api-reference/deployments/list-deployments https://api.onkernel.com/spec.json get /deployments List deployments. Optionally filter by application name and version. # Stream deployment events via SSE Source: https://kernel.sh/docs/api-reference/deployments/stream-deployment-events-via-sse https://api.onkernel.com/spec.json get /deployments/{id}/events Establishes a Server-Sent Events (SSE) stream that delivers real-time logs and status updates for a deployment. The stream terminates automatically once the deployment reaches a terminal state. # Delete extension by ID or name Source: https://kernel.sh/docs/api-reference/extensions/delete-extension-by-id-or-name https://api.onkernel.com/spec.json delete /extensions/{id_or_name} Delete an extension by its ID or by its name. # Download extension archive Source: https://kernel.sh/docs/api-reference/extensions/download-extension-archive https://api.onkernel.com/spec.json get /extensions/{id_or_name} Download the extension as a ZIP archive by ID or name. # Download unpacked extension from Chrome Web Store Source: https://kernel.sh/docs/api-reference/extensions/download-unpacked-extension-from-chrome-web-store https://api.onkernel.com/spec.json get /extensions/from_chrome_store Returns a ZIP archive containing the unpacked extension fetched from the Chrome Web Store. # Get extension metadata Source: https://kernel.sh/docs/api-reference/extensions/get-extension-metadata https://api.onkernel.com/spec.json get /extensions/{id_or_name}/metadata Get an extension's metadata (name, size, timestamps) by ID or name, without downloading the archive. # List browser extensions Source: https://kernel.sh/docs/api-reference/extensions/list-browser-extensions https://api.onkernel.com/spec.json get /extensions List extensions in the resolved project. # Upload a browser extension Source: https://kernel.sh/docs/api-reference/extensions/upload-a-browser-extension https://api.onkernel.com/spec.json post /extensions Upload a zip file containing an unpacked browser extension. Optionally provide a unique name for later reference. # Delete browser sessions for an invocation Source: https://kernel.sh/docs/api-reference/invocations/delete-browser-sessions-for-an-invocation https://api.onkernel.com/spec.json delete /invocations/{id}/browsers Delete all browser sessions created within the specified invocation. # Get invocation details Source: https://kernel.sh/docs/api-reference/invocations/get-invocation-details https://api.onkernel.com/spec.json get /invocations/{id} Get details about an invocation's status and output. # Invoke an action Source: https://kernel.sh/docs/api-reference/invocations/invoke-an-action https://api.onkernel.com/spec.json post /invocations Invoke an action. # List browsers for an invocation Source: https://kernel.sh/docs/api-reference/invocations/list-browsers-for-an-invocation https://api.onkernel.com/spec.json get /invocations/{id}/browsers Returns all active browser sessions created within the specified invocation. # List invocations Source: https://kernel.sh/docs/api-reference/invocations/list-invocations https://api.onkernel.com/spec.json get /invocations List invocations. Optionally filter by application name, action name, status, deployment ID, or start time. # Stream invocation events via SSE Source: https://kernel.sh/docs/api-reference/invocations/stream-invocation-events-via-sse https://api.onkernel.com/spec.json get /invocations/{id}/events Establishes a Server-Sent Events (SSE) stream that delivers real-time logs and status updates for an invocation. The stream terminates automatically once the invocation reaches a terminal state. # Update invocation Source: https://kernel.sh/docs/api-reference/invocations/update-invocation https://api.onkernel.com/spec.json patch /invocations/{id} Update an invocation's status or output. This can be used to cancel an invocation by setting the status to "failed". # Create auth connection Source: https://kernel.sh/docs/api-reference/managed-auth/create-auth-connection https://api.onkernel.com/spec.json post /auth/connections Creates an auth connection for a profile and domain combination. If the provided profile_name does not exist, it is created automatically. Returns 409 Conflict if an auth connection already exists for the given profile and domain. # Delete auth connection Source: https://kernel.sh/docs/api-reference/managed-auth/delete-auth-connection https://api.onkernel.com/spec.json delete /auth/connections/{id} Deletes an auth connection and terminates its workflow. This will: - Delete the auth connection record - Terminate the Temporal workflow - Cancel any in-progress login flows # Get auth connection Source: https://kernel.sh/docs/api-reference/managed-auth/get-auth-connection https://api.onkernel.com/spec.json get /auth/connections/{id} Retrieve an auth connection by its ID. Includes current flow state if a login is in progress. # Get auth connection event timeline Source: https://kernel.sh/docs/api-reference/managed-auth/get-auth-connection-event-timeline https://api.onkernel.com/spec.json get /auth/connections/{id}/timeline Returns a chronological timeline of events for an auth connection — login attempts, automatic re-auth attempts, and health checks. Events are returned newest-first. # List auth connections Source: https://kernel.sh/docs/api-reference/managed-auth/list-auth-connections https://api.onkernel.com/spec.json get /auth/connections List auth connections with optional filters for profile_name and domain. # Start login flow Source: https://kernel.sh/docs/api-reference/managed-auth/start-login-flow https://api.onkernel.com/spec.json post /auth/connections/{id}/login Starts a login flow for the auth connection. Returns immediately with a hosted URL for the user to complete authentication, or triggers automatic re-auth if credentials are stored. # Stream login flow events via SSE Source: https://kernel.sh/docs/api-reference/managed-auth/stream-login-flow-events-via-sse https://api.onkernel.com/spec.json get /auth/connections/{id}/events Establishes a Server-Sent Events (SSE) stream that delivers real-time login flow state updates. The stream terminates automatically once the flow reaches a terminal state (SUCCESS, FAILED, EXPIRED, CANCELED). # Submit field values Source: https://kernel.sh/docs/api-reference/managed-auth/submit-field-values https://api.onkernel.com/spec.json post /auth/connections/{id}/submit Submits field values for the login form. Poll the auth connection to track progress and get results. # Update auth connection Source: https://kernel.sh/docs/api-reference/managed-auth/update-auth-connection https://api.onkernel.com/spec.json patch /auth/connections/{id} Update an auth connection's configuration. Only the fields provided will be updated. # Get organization entitlements Source: https://kernel.sh/docs/api-reference/organization/get-organization-entitlements https://api.onkernel.com/spec.json get /org/entitlements Get the authenticated organization's effective feature access and constraints after applying its plan, active trial treatment, plan status, and organization-specific overrides. Null constraint values mean unlimited. # Get organization limits Source: https://kernel.sh/docs/api-reference/organization/get-organization-limits https://api.onkernel.com/spec.json get /org/limits Get the organization's effective limits and managed auth and vault usage. # Update organization limits Source: https://kernel.sh/docs/api-reference/organization/update-organization-limits https://api.onkernel.com/spec.json patch /org/limits Set the default per-project concurrency cap applied to projects without an explicit override. Set the value to 0 to remove the default; omit to leave it unchanged. The default cannot exceed the organization's concurrency limit. # Create a new profile Source: https://kernel.sh/docs/api-reference/profiles/create-a-new-profile https://api.onkernel.com/spec.json post /profiles Create a browser profile that can be used to load state into future browser sessions. # Delete profile by ID or name Source: https://kernel.sh/docs/api-reference/profiles/delete-profile-by-id-or-name https://api.onkernel.com/spec.json delete /profiles/{id_or_name} Delete a profile by its ID or by its name. # Download profile archive Source: https://kernel.sh/docs/api-reference/profiles/download-profile-archive https://api.onkernel.com/spec.json get /profiles/{id_or_name}/download Downloads the profile in its stored format by default. Current profiles are returned as zstd-compressed tar archives, while legacy profiles remain JSON. Set `format=tar` to decompress current profiles during download; legacy profiles remain JSON. # Get profile by ID or name Source: https://kernel.sh/docs/api-reference/profiles/get-profile-by-id-or-name https://api.onkernel.com/spec.json get /profiles/{id_or_name} Retrieve details for a single profile by its ID or name. # List profiles Source: https://kernel.sh/docs/api-reference/profiles/list-profiles https://api.onkernel.com/spec.json get /profiles List profiles with optional filtering and pagination. # Rename profile by ID or name Source: https://kernel.sh/docs/api-reference/profiles/rename-profile-by-id-or-name https://api.onkernel.com/spec.json patch /profiles/{id_or_name} Update a profile's name. Names must be unique within the logical project; during the default-project migration, unscoped profiles and profiles in the org default project are treated as the same project. Duplicate-name conflicts are checked before update but are best-effort because there is no backing unique index. Renaming a profile while a browser session references it by name may prevent that session's changes from saving; prefer renaming when the profile is not in use. # Create a project Source: https://kernel.sh/docs/api-reference/projects/create-a-project https://api.onkernel.com/spec.json post /org/projects Create a new project within the authenticated organization. # Create a project Source: https://kernel.sh/docs/api-reference/projects/create-a-project-1 https://api.onkernel.com/spec.json post /projects Deprecated: use `POST /org/projects` instead. This route will be removed on 2026-11-24. Create a new project within the authenticated organization. # Delete a project by ID or name Source: https://kernel.sh/docs/api-reference/projects/delete-a-project-by-id-or-name https://api.onkernel.com/spec.json delete /org/projects/{id_or_name} Soft-delete a project. The project must be empty (no active resources). # Delete a project by ID or name Source: https://kernel.sh/docs/api-reference/projects/delete-a-project-by-id-or-name-1 https://api.onkernel.com/spec.json delete /projects/{id_or_name} Deprecated: use `DELETE /org/projects/{id_or_name}` instead. This route will be removed on 2026-11-24. Soft-delete a project. The project must be empty (no active resources). # Get a project by ID or name Source: https://kernel.sh/docs/api-reference/projects/get-a-project-by-id-or-name https://api.onkernel.com/spec.json get /org/projects/{id_or_name} Get a project by its ID or by its name. Names are unique within an organization. # Get a project by ID or name Source: https://kernel.sh/docs/api-reference/projects/get-a-project-by-id-or-name-1 https://api.onkernel.com/spec.json get /projects/{id_or_name} Deprecated: use `GET /org/projects/{id_or_name}` instead. This route will be removed on 2026-11-24. Get a project by ID or name. # Get project limits by ID or name Source: https://kernel.sh/docs/api-reference/projects/get-project-limits-by-id-or-name https://api.onkernel.com/spec.json get /org/projects/{id_or_name}/limits Get the resource limit overrides for a project. Null values mean no project-level cap (org limit applies). # Get project limits by ID or name Source: https://kernel.sh/docs/api-reference/projects/get-project-limits-by-id-or-name-1 https://api.onkernel.com/spec.json get /projects/{id_or_name}/limits Deprecated: use `GET /org/projects/{id_or_name}/limits` instead. This route will be removed on 2026-11-24. Get the resource limit overrides for a project. Null values mean no project-level cap (org limit applies). # List projects Source: https://kernel.sh/docs/api-reference/projects/list-projects https://api.onkernel.com/spec.json get /org/projects List projects for the authenticated organization. # List projects Source: https://kernel.sh/docs/api-reference/projects/list-projects-1 https://api.onkernel.com/spec.json get /projects Deprecated: use `GET /org/projects` instead. This route will be removed on 2026-11-24. List projects for the authenticated organization. # Update a project by ID or name Source: https://kernel.sh/docs/api-reference/projects/update-a-project-by-id-or-name https://api.onkernel.com/spec.json patch /org/projects/{id_or_name} Update a project's name or status. # Update a project by ID or name Source: https://kernel.sh/docs/api-reference/projects/update-a-project-by-id-or-name-1 https://api.onkernel.com/spec.json patch /projects/{id_or_name} Deprecated: use `PATCH /org/projects/{id_or_name}` instead. This route will be removed on 2026-11-24. Update a project's name or status. # Update project limits by ID or name Source: https://kernel.sh/docs/api-reference/projects/update-project-limits-by-id-or-name https://api.onkernel.com/spec.json patch /org/projects/{id_or_name}/limits Update resource limit overrides for a project. Only fields present in the request are modified. Set a field to 0 to remove that limit cap; omit a field to leave it unchanged. # Update project limits by ID or name Source: https://kernel.sh/docs/api-reference/projects/update-project-limits-by-id-or-name-1 https://api.onkernel.com/spec.json patch /projects/{id_or_name}/limits Deprecated: use `PATCH /org/projects/{id_or_name}/limits` instead. This route will be removed on 2026-11-24. Update resource limit overrides for a project. Only fields present in the request are modified. Set a field to 0 to remove that limit cap; omit a field to leave it unchanged. # Check proxy health Source: https://kernel.sh/docs/api-reference/proxies/check-proxy-health https://api.onkernel.com/spec.json post /proxies/{id}/check Run a health check on the proxy to verify it's working. Optionally specify a URL to test reachability against a specific target. For ISP and datacenter proxies, this reliably tests whether the target site is reachable from the proxy's stable exit IP. For residential and mobile proxies, the exit node varies between requests, so this validates proxy configuration and connectivity rather than guaranteeing site-specific reachability. # Create a proxy Source: https://kernel.sh/docs/api-reference/proxies/create-a-proxy https://api.onkernel.com/spec.json post /proxies Create a new proxy configuration in the resolved project. # Delete proxy by ID Source: https://kernel.sh/docs/api-reference/proxies/delete-proxy-by-id https://api.onkernel.com/spec.json delete /proxies/{id} Soft delete a proxy. Session records referencing it are not modified. If egress binding polling is enabled, existing tunnels for active sessions using the proxy are terminated within one polling interval; subsequent connections through the deleted proxy are rejected. # Get proxy by ID Source: https://kernel.sh/docs/api-reference/proxies/get-proxy-by-id https://api.onkernel.com/spec.json get /proxies/{id} Retrieve a proxy in the resolved project by ID. # List proxies Source: https://kernel.sh/docs/api-reference/proxies/list-proxies https://api.onkernel.com/spec.json get /proxies List proxies in the resolved project. # Rename proxy by ID Source: https://kernel.sh/docs/api-reference/proxies/rename-proxy-by-id https://api.onkernel.com/spec.json patch /proxies/{id} Update a proxy's name. Proxy names are not unique and are not ID-or-name addressable on this endpoint; duplicate names are allowed. Name-based session-create lookups can remain ambiguous until callers resolve proxies by ID or the API adds a stronger uniqueness contract. # Create or retrieve a vault by immutable name Source: https://kernel.sh/docs/api-reference/vaults/create-or-retrieve-a-vault-by-immutable-name https://api.onkernel.com/spec.json post /vaults Free organizations can store up to 3 non-deleted vaults across all projects. Paid plans and active trials have no vault cap. Retrieving an existing vault by name succeeds even at the limit. # Create or retrieve an identical vault item by immutable key Source: https://kernel.sh/docs/api-reference/vaults/create-or-retrieve-an-identical-vault-item-by-immutable-key https://api.onkernel.com/spec.json put /vaults/{id_or_name}/items/{key} # Delete a vault and invalidate its items Source: https://kernel.sh/docs/api-reference/vaults/delete-a-vault-and-invalidate-its-items https://api.onkernel.com/spec.json delete /vaults/{id_or_name} # Delete a vault item and invalidate its secret value Source: https://kernel.sh/docs/api-reference/vaults/delete-a-vault-item-and-invalidate-its-secret-value https://api.onkernel.com/spec.json delete /vaults/{id_or_name}/items/{key} # Get a vault Source: https://kernel.sh/docs/api-reference/vaults/get-a-vault https://api.onkernel.com/spec.json get /vaults/{id_or_name} # Get a vault item, its currently available operations, and optionally expanded live data Source: https://kernel.sh/docs/api-reference/vaults/get-a-vault-item-its-currently-available-operations-and-optionally-expanded-live-data https://api.onkernel.com/spec.json get /vaults/{id_or_name}/items/{key} The response advertises operations that are valid in the item's current state and live data that can be requested through `expand`. Read each operation's description before using it. Expanded data is fetched from the provider and is not persisted in the vault item. Requesting an unavailable expansion returns 409 instead of a partial item. # List immutable audit events for a vault item Source: https://kernel.sh/docs/api-reference/vaults/list-immutable-audit-events-for-a-vault-item https://api.onkernel.com/spec.json get /vaults/{id_or_name}/items/{key}/events # List vault items without secret values Source: https://kernel.sh/docs/api-reference/vaults/list-vault-items-without-secret-values https://api.onkernel.com/spec.json get /vaults/{id_or_name}/items # List vaults in the current project Source: https://kernel.sh/docs/api-reference/vaults/list-vaults-in-the-current-project https://api.onkernel.com/spec.json get /vaults # Perform an operation advertised by a vault item Source: https://kernel.sh/docs/api-reference/vaults/perform-an-operation-advertised-by-a-vault-item https://api.onkernel.com/spec.json post /vaults/{id_or_name}/items/{key}/operations Retrieve the item first and invoke only an operation listed in `available_operations`, following its natural-language description. Operations may call an external provider and can return the item's updated state. If the provider rate limits spend-request creation, returns HTTP 429 with code `spend_request_rate_limited`; stop and back off before retrying. # Update a card specification before or between authorizations Source: https://kernel.sh/docs/api-reference/vaults/update-a-card-specification-before-or-between-authorizations https://api.onkernel.com/spec.json patch /vaults/{id_or_name}/items/{key} # Deploying Source: https://kernel.sh/docs/apps/deploy Kernel's app deployment process is as simple as it is fast. There are no configuration files to manage or complex CI/CD pipelines. Once you deploy an app on Kernel, you can schedule its actions on a job or run them from other contexts. You can even run actions multiple times in parallel. ## Deploy the app ### From local directory Use our CLI from the root directory of your project: ```bash theme={null} kernel deploy ``` #### Notes * The `entrypoint_file_name` is the file name where you [defined](/docs/apps/develop) your app. * Include a `.gitignore` file to exclude dependency folders like `node_modules` and `.venv`. ### From GitHub You can deploy a Kernel app directly from a public or private GitHub repository using the Kernel CLI. No need to clone or manually push code. ```bash theme={null} kernel deploy github \ --url https://github.com// \ --ref \ --entrypoint \ [--path ] \ [--github-token ] \ [--env KEY=value ...] \ [--env-file .env] \ [--version latest] \ [--force] ``` #### Notes * **`--path` vs `--entrypoint`:** Use `--path` to specify a subdirectory within the repo (useful for monorepos), and `--entrypoint` for the path to your app's entry file relative to that directory (or repo root if no `--path` is specified). * The CLI automatically downloads and extracts the GitHub source code and uploads your app for deployment. * For private repositories, provide a `--github-token` or set the `GITHUB_TOKEN` environment variable. ## Environment variables You can set environment variables for your app using the `--env` flag. For example: ```bash Typescript/Javascript (inline) theme={null} kernel deploy my_app.ts --env MY_ENV_VAR=my_value # Can add multiple env vars delimited by space ``` ```bash Typescript/Javascript (from file) theme={null} kernel deploy my_app.ts --env-file .env ``` ```bash Python (inline) theme={null} kernel deploy my_app.py --env MY_ENV_VAR=my_value # Can add multiple env vars delimited by space ``` ```bash Python (from file) theme={null} kernel deploy my_app.py --env-file .env ``` ### Reserved environment variables Kernel injects a few environment variables into every deployment and its invocations. These names are **reserved** — if you set them via `--env` or `--env-file`, Kernel overrides your value, so setting them has no effect: * `KERNEL_API_KEY` — a per-deployment API key Kernel mints at deploy time (see [Deployment API keys](/docs/info/api-keys#deployment-api-keys)). The SDKs read it from the environment by default, so your app authenticates with this key automatically. * `ENTRYPOINT_RELPATH` — set by the platform to locate your entrypoint. #### Using a different key for your app's calls You can't change `KERNEL_API_KEY` itself, but you can have your app authenticate with a different key — say a long-lived org- or project-scoped key that outlives any single deployment. Put it in a **non-reserved** variable and pass it to the client explicitly: ```python Python theme={null} import os from kernel import Kernel # Use your own key from a non-reserved var instead of the injected deployment key. client = Kernel(api_key=os.environ["MY_KERNEL_API_KEY"]) ``` ```typescript TypeScript theme={null} import Kernel from '@onkernel/sdk'; const client = new Kernel({ apiKey: process.env.MY_KERNEL_API_KEY }); ``` Now the API calls your app makes go out as your key. The deployment key stays in place for Kernel's own use — running the invocation and reporting its result — so your key only needs permissions for the calls you actually make. ## Deployment notes * **The dependency manifest (`package.json` for JS/TS, `pyproject.toml` for Python) must be present in the root directory of your project.** * **For JS/TS apps, set `"type": "module"` in your `package.json`.** * View deployment logs using: `kernel deploy logs --follow` * If you encounter a 500 error during deployment, verify that your entrypoint file name and extension are correct (e.g., `app.py` not `app` or `app.js`). * Kernel assumes the root directory contains at least this file structure: ```bash Typescript/Javascript theme={null} project-root/ ├─ .gitignore # Exclude dependency folders like node_modules ├─ my_app.ts # Entrypoint file (can be located in a subdirectory, e.g. src/my_app.ts) ├─ package.json ├─ tsconfig.json # If using TypeScript └─ bun.lock | package-lock.json | pnpm-lock.yaml # One of these lockfiles ``` ```bash Python theme={null} project-root/ ├─ .gitignore # Exclude dependency folders like .venv ├─ my_app.py # Entrypoint file └─ pyproject.toml ``` ```bash theme={null} # Successful deployment CLI output SUCCESS Compressed files SUCCESS Deployment successful SUCCESS App "my_app.ts" deployed with action(s): [my-action] INFO Invoke with: kernel invoke my-app my-action --payload '{...}' SUCCESS Total deployment time: 2.78s ``` Once deployed, you can [invoke](/docs/apps/invoke) your app from anywhere. # Developing Source: https://kernel.sh/docs/apps/develop In addition to our browser API, Kernel provides a code execution platform for deploying and invoking code. Typically, Kernel's code execution platform is used for deploying and invoking browser automations or web agents. When using Kernel's code execution platform, we co-locate your code with any Kernel browser environments you instantiate in your app. This solves common issues with browser connections over CDP: * **Reduced latency:** Your code runs directly alongside the browser, reducing round-trip latency * **Improved reliability:** Fewer unexpected disconnects between your code and browser * **Higher throughput:** Eliminates bandwidth bottlenecks during data-intensive operations like screenshots Install our [MCP server](/docs/reference/mcp-server) to give your coding agent our `search_docs` tool. ## Apps, Actions, and Invocations An `App` is a codebase deployed on Kernel. You can deploy any codebase in Typescript or Python on Kernel. An `Action` is an invokable method within an app. Actions allow your to register entry points or functions that can be triggered on-demand. Actions can call non-action methods. Apps can have multiple actions. An `Invocation` is a single execution of an action. Invocations can be triggered via API, scheduled as a job, or run on-demand. ## Getting started: create an app First, install the Kernel SDK for your language: ```bash Typescript/Javascript theme={null} npm install @onkernel/sdk ``` ```bash Python theme={null} uv pip install kernel ``` Then create an app: ```typescript Typescript/Javascript theme={null} import Kernel, { type KernelContext } from '@onkernel/sdk'; const kernel = new Kernel(); const app = kernel.app('my-app-name'); ``` ```python Python theme={null} from kernel import Kernel, App, KernelContext kernel = Kernel() app = App("my-app-name") ``` Then, define and register an action that you want to invoke. ## Registering actions Action methods receive two parameters: * `runtimeContext`: Contextual information provided by Kernel during execution * `payload`: Optional runtime data that you provide when invoking the action (max 64 KB). [Read more](/docs/apps/invoke#payload-parameter) You can register actions using either approach: ### Inline definition (recommended) ```typescript Typescript/Javascript theme={null} app.action('my-action-name', async (ctx: KernelContext, payload) => { const { tshirt_size, color, shipping_address } = payload; // Your action logic here return { order_id: 'example-order-id' }; }); ``` ```python Python theme={null} @app.action("my-action-name") async def my_action_method(ctx: KernelContext, payload): tshirt_size = payload["tshirt_size"] color = payload["color"] shipping_address = payload["shipping_address"] # Your action logic here return {"order_id": "example-order-id"} ``` ### Define then register This approach is better for larger apps, unit testing, and team collaboration since functions can be tested independently and reused across multiple actions. ```typescript Typescript/Javascript theme={null} const myActionMethod = async (ctx: KernelContext, payload) => { const { tshirt_size, color, shipping_address } = payload; // Your action logic here return { order_id: 'example-order-id' }; }; app.action('my-action-name', myActionMethod); ``` ```python Python theme={null} async def my_action_method(ctx: KernelContext, payload): tshirt_size = payload["tshirt_size"] color = payload["color"] shipping_address = payload["shipping_address"] # Your action logic here return {"order_id": "example-order-id"} app.action("my-action-name")(my_action_method) ``` ## Environment variables You can set environment variables when [deploying](/docs/apps/deploy#environment-variables) your app. They then can be accessed in the usual way: ```typescript Typescript/Javascript theme={null} const ENV_VAR = process.env.ENV_VAR; const myActionMethod = async (runtimeContext, payload) => { // ... }; ``` ```python Python theme={null} import os ENV_VAR = os.getenv("ENV_VAR") def my_action_method(runtime_context, payload): # ... ``` ## Return values Action methods can return values, which will be included in the invocation's final response. ```typescript Typescript/Javascript theme={null} const myActionMethod = async (runtimeContext, payload) => { const { tshirt_size, color, shipping_address } = payload; // ... return { order_id: "example-order-id", } }; ``` ```python Python theme={null} def my_action_method(runtime_context, payload): tshirt_size, color, shipping_address = ( payload["tshirt_size"], payload["color"], payload["shipping_address"] ) # ... return {"order_id": "example-order-id"} ``` The examples above show actions returning data. ## Building browser automations with Kernel apps To implement a browser automation or web agent, instantiate an app and define an action that creates a Kernel browser. Kernel browsers launch with a default context and page. Make sure to access the [existing context and page](https://playwright.dev/docs/api/class-browsertype#browser-type-connect-over-cdp) (`contexts()[0]` and `pages()[0]`), rather than trying to create a new one. ```typescript Typescript/Javascript theme={null} import Kernel, { type KernelContext } from '@onkernel/sdk'; import { chromium } from 'playwright'; const kernel = new Kernel(); const app = kernel.app('browser-automation'); app.action('get-page-title', async (ctx: KernelContext, payload) => { const kernelBrowser = await kernel.browsers.create({ invocation_id: ctx.invocation_id, }); 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()); try { await page.goto('https://www.google.com'); const title = await page.title(); return { title }; } finally { await browser.close(); } }); ``` ```python Python theme={null} from kernel import Kernel, App, KernelContext from playwright.async_api import async_playwright kernel = Kernel() app = App("browser-automation") @app.action("get-page-title") async def get_page_title(ctx: KernelContext, payload): kernel_browser = kernel.browsers.create(invocation_id=ctx.invocation_id) 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() try: await page.goto("https://www.google.com") title = await page.title() return {"title": title} finally: await browser.close() ``` Web agent frameworks sometimes require environment variables (e.g. LLM API keys). Set them when [deploying](/docs/apps/deploy#environment-variables) your app. ## Next steps Once you're happy with your app, follow [these steps](/docs/apps/deploy) to deploy and invoke it on the Kernel platform. # Invoking Source: https://kernel.sh/docs/apps/invoke ## Via API You can invoke your app by making a `POST` request to Kernel's API or via the CLI. Both support passing a payload. **For automations and agents that take longer than 100 seconds, use [async invocations](/docs/apps/invoke#asynchronous-invocations).** Synchronous invocations time out after 100 seconds. ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); const invocation = await kernel.invocations.create({ action_name: 'analyze', app_name: 'my-app', version: '1.0.0', }); console.log(invocation.id); ``` ```python Python theme={null} from kernel import Kernel kernel = Kernel() invocation = kernel.invocations.create( action_name="analyze", app_name="my-app", version="1.0.0", ) print(invocation.id) ``` ```go Go theme={null} package main import ( "context" "fmt" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() invocation, err := client.Invocations.New(ctx, kernel.InvocationNewParams{ ActionName: "analyze", AppName: "my-app", Version: "1.0.0", }) if err != nil { panic(err) } fmt.Println(invocation.ID) } ``` ### Asynchronous invocations For long running jobs, use asynchronous invocations to trigger Kernel actions without waiting for the result. You can then stream real-time [status updates](/docs/apps/status#streaming-status-updates) for the result. Asynchronous invocations time out after 15 minutes by default but can be configured to last up to 1 hour by setting the optional `async_timeout_seconds` parameter during invocation. ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); const invocation = await kernel.invocations.create({ async: true, action_name: 'analyze', app_name: 'my-app', version: '1.0.0', }); console.log(invocation.id); ``` ```python Python theme={null} from kernel import Kernel kernel = Kernel() invocation = kernel.invocations.create( action_name="analyze", app_name="my-app", version="1.0.0", async_=True, ) print(invocation.id) ``` ```go Go theme={null} package main import ( "context" "fmt" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() invocation, err := client.Invocations.New(ctx, kernel.InvocationNewParams{ Async: kernel.Bool(true), ActionName: "analyze", AppName: "my-app", Version: "1.0.0", }) if err != nil { panic(err) } fmt.Println(invocation.ID) } ``` ## Via CLI Invoke an app action immediately via the CLI: ```bash theme={null} kernel invoke ``` ### Payload parameter `--payload` allows you to invoke the action with specified parameters. This enables your action to receive and handle dynamic inputs at runtime. For example: Payloads are stringified JSON and have a maximum size of 4.5 MB. ```bash theme={null} kernel invoke --payload '{"tshirt_size": "small", "color": "black", "shipping_address": "2 Mint Plz, San Francisco CA 94103"}' ``` See [here](/docs/apps/develop#parameters) to learn how to access the payload in your action method. ### Return values If your action specifies a [return value](/docs/apps/develop#return-values), the invocation returns its value once it completes. (The Kernel CLI uses asynchronous invocations under the hood) App invocations accrue compute usage separately from any browsers they create. See the [pricing FAQ](/docs/info/pricing#faq) for how app invocations are charged. # Logs Source: https://kernel.sh/docs/apps/logs ## Via API After you [invoke](/docs/apps/invoke) an action, you can stream the invocation's logs in real time: ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); const logs = await kernel.invocations.follow(invocation_id); ``` ```python Python theme={null} from kernel import Kernel kernel = Kernel() logs = kernel.invocations.follow(invocation_id) ``` ```go Go theme={null} package main import ( "context" "fmt" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() logs := client.Invocations.FollowStreaming(ctx, "inv_123", kernel.InvocationFollowParams{}) defer logs.Close() for logs.Next() { event := logs.Current() if event.Event == "log" { fmt.Println(event.Message) } } if err := logs.Err(); err != nil { panic(err) } } ``` Log lines will be truncated to 64KiB. For large payloads write data to external storage and log a reference instead. ### Example Here's an example showing how to handle streaming logs: ```typescript Typescript/Javascript theme={null} const follow = await kernel.invocations.follow(invocation.id); for await (const evt of follow) { if (evt.event === 'log') { console.log(`[${evt.timestamp}] ${evt.message}`); } else if (evt.event === 'error') { console.error('Error:', evt.error.message); break; } else if (evt.event === 'invocation_state') { if (evt.invocation.status === 'succeeded' || evt.invocation.status === 'failed') { break; } } } ``` ## Via CLI You can also stream the logs to your terminal via the CLI: ```bash theme={null} kernel logs --follow ``` If you don't specify `--follow`, the logs will print to the terminal until 3 seconds of inactivity and then stops. You can get logs for a specific invocation by adding: ``` -i --invocation Show logs for a specific invocation of the app. ``` # Secrets Source: https://kernel.sh/docs/apps/secrets There are multiple ways to pass secrets and API keys to your Kernel app: ## 1. Deployment environment variables Deploy your app with secrets as [environment variables](/docs/apps/deploy#environment-variables). Your app can then access them at runtime. You can set environment variables in two ways: * **`--env` flag**: Pass individual key-value pairs directly in the command * **`--env-file` flag**: Load variables from a `.env` file ```bash theme={null} # Using --env flag for individual variables kernel deploy my_app.ts --env OPENAI_API_KEY=sk-... --env ANTHROPIC_API_KEY=sk-ant-... # Using --env-file to load from a file kernel deploy my_app.ts --env-file .env # Combine both approaches kernel deploy my_app.ts --env-file .env --env OPENAI_API_KEY=sk-... ``` Then access the variables in your app: ```typescript TypeScript theme={null} import Anthropic from "@anthropic-ai/sdk"; import OpenAI from "openai"; app.action('ai-action', async (ctx: KernelContext) => { // Access API keys from environment variables const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY, }); const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, }); // Use the clients... }); ``` ```python Python theme={null} import os from anthropic import Anthropic from openai import OpenAI @app.action("ai-action") async def ai_action(ctx: KernelContext): # Access API keys from environment variables anthropic = Anthropic( api_key=os.environ.get("ANTHROPIC_API_KEY"), ) openai = OpenAI( api_key=os.environ.get("OPENAI_API_KEY"), ) # Use the clients... ``` ## 2. Runtime variables For use cases where different API keys are needed per invocation (such as platforms using end-user keys), pass the secrets at runtime using the [payload parameter](/docs/apps/invoke#payload-parameter). Use encryption standards in your app to protect sensitive data. ```typescript TypeScript theme={null} import OpenAI from "openai"; app.action('ai-action', async (ctx: KernelContext, payload) => { // Decrypt the API key passed at runtime const apiKey = decrypt(payload.encryptedApiKey); const openai = new OpenAI({ apiKey: apiKey, }); // Use the client with the user's API key... }); ``` ```python Python theme={null} from openai import OpenAI @app.action("ai-action") async def ai_action(ctx: KernelContext, payload): # Decrypt the API key passed at runtime api_key = decrypt(payload["encryptedApiKey"]) openai = OpenAI( api_key=api_key, ) # Use the client with the user's API key... ``` # Status Source: https://kernel.sh/docs/apps/status Once you've [deployed](/docs/apps/deploy) an app and invoked it, you can monitor its status using streaming for real-time updates or polling for periodic checks. An invocation ends once its code execution finishes. ## Streaming Status Updates For real-time status monitoring, use `follow` to [stream invocation events](https://kernel.sh/docs/api-reference/invocations/stream-invocation-events-via-sse). This provides immediate updates as your invocation progresses and is more efficient than polling. ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); const response = await kernel.invocations.follow('id'); console.log(response); ``` ```python Python theme={null} from kernel import Kernel kernel = Kernel() response = kernel.invocations.follow(id="id") print(response) ``` ```go Go theme={null} package main import ( "context" "fmt" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() stream := client.Invocations.FollowStreaming(ctx, "id", kernel.InvocationFollowParams{}) defer stream.Close() for stream.Next() { event := stream.Current() if event.Event == "invocation_state" { fmt.Println(event.Invocation.Status) } } if err := stream.Err(); err != nil { panic(err) } } ``` ### Example Here's an example showing how to handle streaming status updates: ```typescript Typescript/Javascript theme={null} const result = await kernel.invocations.retrieve(invocation.id); const follow = await kernel.invocations.follow(result.id); for await (const evt of follow) { if (evt.event === 'invocation_state') { console.log(`Status: ${evt.invocation.status}`); if (evt.invocation.status === 'succeeded') { console.log('Invocation completed successfully'); if (evt.invocation.output) { console.log('Result:', JSON.parse(evt.invocation.output)); } break; } else if (evt.invocation.status === 'failed') { console.log('Invocation failed'); if (evt.invocation.status_reason) { console.log('Error:', evt.invocation.status_reason); } break; } } else if (evt.event === 'error') { console.error('Error:', evt.error.message); break; } } ``` ## Polling Status Updates Alternatively, you can poll the status endpoint using `retrieve` to check the invocation status periodically. ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); const invocation = await kernel.invocations.retrieve('rr33xuugxj9h0bkf1rdt2bet'); console.log(invocation.status); ``` ```python Python theme={null} from kernel import Kernel kernel = Kernel() invocation = kernel.invocations.retrieve("rr33xuugxj9h0bkf1rdt2bet") print(invocation.status) ``` ```go Go theme={null} package main import ( "context" "fmt" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() invocation, err := client.Invocations.Get(ctx, "rr33xuugxj9h0bkf1rdt2bet") if err != nil { panic(err) } fmt.Println(invocation.Status) } ``` # Stopping Source: https://kernel.sh/docs/apps/stop You can terminate an invocation that's running. This is useful for stopping automations or agents stuck in an infinite loop. Terminating an invocation also destroys any browsers associated with it. ## Via API You can stop an invocation by setting its status to `failed`. This will cancel the invocation and mark it as terminated. ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); const invocation = await kernel.invocations.update('invocation_id', { status: 'failed', output: JSON.stringify({ error: 'Invocation cancelled by user' }), }); ``` ```python Python theme={null} from kernel import Kernel kernel = Kernel() invocation = kernel.invocations.update( id="invocation_id", status="failed", output='{"error":"Invocation cancelled by user"}', ) ``` ```go Go theme={null} package main import ( "context" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() invocation, err := client.Invocations.Update(ctx, "invocation_id", kernel.InvocationUpdateParams{ Status: kernel.InvocationUpdateParamsStatusFailed, Output: kernel.String(`{"error":"Invocation cancelled by user"}`), }) if err != nil { panic(err) } _ = invocation } ``` ## Via CLI Use `ctrl-c` in the terminal tab where you launched the invocation. # Connection Configuration Source: https://kernel.sh/docs/auth/configuration Shared options for managed auth connections, regardless of integration flow Managed Auth connections use the same configuration whether you collect credentials through the [Hosted UI](/docs/auth/hosted-ui), the [React component](/docs/auth/react), or the [programmatic flow](/docs/auth/programmatic). These options apply to the initial login, every background health check, and each automatic reauthentication attempt. ## Credentials and Auto-Reauth By default, Kernel saves durable credential fields after a successful login. Kernel can automatically reauthenticate credential-only flows and attempts to provide TOTP codes when needed. Submitted one-time codes (TOTP, SMS, etc.) aren't saved. To opt out of credential saving, set `save_credentials: false` when creating the connection. See [Credentials](/docs/auth/credentials) for configuration examples. Automatic re-authentication is gated by two boolean flags that both default to `true`: * `health_checks` — whether the connection runs periodic health checks at all. When `false`, the system never automatically verifies the session and never triggers reauth on its own. * `auto_reauth` — whether a failed scheduled health check is allowed to attempt re-authentication. When `false`, expired sessions are marked `NEEDS_AUTH` instead of being repaired automatically. `auto_reauth` only has an effect on the automatic flow when `health_checks` is also `true`, because reauth is triggered by a failing scheduled health check. Manually triggering a health check via the API still works regardless of `health_checks`. ```typescript TypeScript theme={null} const auth = await kernel.auth.connections.create({ domain: 'example.com', profile_name: 'my-profile', health_checks: false, auto_reauth: false, }); ``` ```python Python theme={null} auth = await kernel.auth.connections.create( domain="example.com", profile_name="my-profile", health_checks=False, auto_reauth=False, ) ``` ```go Go theme={null} auth, err := client.Auth.Connections.New(ctx, kernel.AuthConnectionNewParams{ ManagedAuthCreateRequest: kernel.ManagedAuthCreateRequestParam{ Domain: "example.com", ProfileName: "my-profile", HealthChecks: kernel.Bool(false), AutoReauth: kernel.Bool(false), }, }) if err != nil { panic(err) } _ = auth ``` Both flags can be flipped on an existing connection with `auth.connections.update`; changes take effect immediately on the running connection. Automatic reauthentication requires a previously successful login and saved credentials for the durable login fields. Setting `auto_reauth: true` permits Kernel to attempt it but doesn't guarantee the next login will succeed. If Kernel can't complete an automatic attempt, the connection transitions to `NEEDS_AUTH` so you can start a new login. ## Custom Login URL If the site's login page isn't at the default location, specify it when creating the connection: ```typescript TypeScript theme={null} const auth = await kernel.auth.connections.create({ domain: 'example.com', profile_name: 'my-profile', login_url: 'https://example.com/auth/signin', }); ``` ```python Python theme={null} auth = await kernel.auth.connections.create( domain="example.com", profile_name="my-profile", login_url="https://example.com/auth/signin", ) ``` ```go Go theme={null} auth, err := client.Auth.Connections.New(ctx, kernel.AuthConnectionNewParams{ ManagedAuthCreateRequest: kernel.ManagedAuthCreateRequestParam{ Domain: "example.com", ProfileName: "my-profile", LoginURL: kernel.String("https://example.com/auth/signin"), }, }) if err != nil { panic(err) } _ = auth ``` ## SSO/OAuth Support Managed Auth supports common "Sign in with Google/GitHub/Microsoft" flows. The user completes the OAuth flow with the provider, and Kernel saves the authenticated session to the profile. Automatic reauthentication depends on the provider's login requirements. See [Can this connection auto-reauth?](/docs/auth/connection-lifecycle#can-this-connection-auto-reauth) for how Kernel determines eligibility. Common SSO provider domains are automatically allowed by default, including Google, Microsoft/Azure AD, Okta, Auth0, Apple, GitHub, Facebook, LinkedIn, Amazon Cognito, OneLogin, and Ping Identity. You don't need to add these to `allowed_domains`. For custom or less common OAuth providers, add their domains to `allowed_domains`: ```typescript TypeScript theme={null} const auth = await kernel.auth.connections.create({ domain: 'example.com', profile_name: 'my-profile', allowed_domains: ['sso.custom-provider.com'], }); ``` ```python Python theme={null} auth = await kernel.auth.connections.create( domain="example.com", profile_name="my-profile", allowed_domains=["sso.custom-provider.com"], ) ``` ```go Go theme={null} auth, err := client.Auth.Connections.New(ctx, kernel.AuthConnectionNewParams{ ManagedAuthCreateRequest: kernel.ManagedAuthCreateRequestParam{ Domain: "example.com", ProfileName: "my-profile", AllowedDomains: []string{"sso.custom-provider.com"}, }, }) if err != nil { panic(err) } _ = auth ``` ## Custom Proxy Pin the auth flow to a specific [proxy](/docs/proxies/overview) so logins, health checks, and automatic re-authentications all egress through that proxy. This is useful for sites that allowlist IPs, geo-pin sessions, or treat IP changes as a fraud signal. How stable the exit IP is depends on the proxy type: * **[ISP](/docs/proxies/isp)** and **[datacenter](/docs/proxies/datacenter)** proxies provide a stable exit IP within a single session, but Kernel does not guarantee the same IP across sessions. Sites with adaptive auth that trigger a step-up challenge (one-time code, device verification) when the client IP changes may flag the IP shift between the initial login and a subsequent health check or reauth. * **[Residential](/docs/proxies/residential)** proxies rotate IPs per connection — use them when you need legitimacy from a real ISP pool but can tolerate IP changes. * **[Custom (BYO)](/docs/proxies/custom)** proxies route through whatever you point them at, so this is the right pick if you need a truly static IP that persists across the initial login and every subsequent health check and reauth (e.g. an allowlisted egress your security team owns). Create a proxy first, then attach it to the connection: ```typescript TypeScript theme={null} const proxy = await kernel.proxies.create({ type: 'isp' }); const auth = await kernel.auth.connections.create({ domain: 'example.com', profile_name: 'my-profile', proxy: { id: proxy.id }, }); ``` ```python Python theme={null} proxy = kernel.proxies.create(type="isp") auth = await kernel.auth.connections.create( domain="example.com", profile_name="my-profile", proxy={"id": proxy.id}, ) ``` ```go Go theme={null} proxy, err := client.Proxies.New(ctx, kernel.ProxyNewParams{ Type: kernel.ProxyNewParamsTypeIsp, }) if err != nil { panic(err) } auth, err := client.Auth.Connections.New(ctx, kernel.AuthConnectionNewParams{ ManagedAuthCreateRequest: kernel.ManagedAuthCreateRequestParam{ Domain: "example.com", ProfileName: "my-profile", Proxy: kernel.ManagedAuthCreateRequestProxyParam{ ID: kernel.String(proxy.ID), }, }, }) if err != nil { panic(err) } _ = auth ``` You can also reference a proxy by `name` instead of `id`. The proxy must belong to the same org and project as the connection. Once attached, every browser the connection spins up — the initial login, every background health check, and every automatic re-auth — runs through that proxy. You can swap the proxy on an existing connection with `auth.connections.update`; the change takes effect immediately, so the next health check or reauth uses the new proxy. ```typescript TypeScript theme={null} await kernel.auth.connections.update(auth.id, { proxy: { id: newProxy.id }, }); ``` ```python Python theme={null} await kernel.auth.connections.update( auth.id, proxy={"id": new_proxy.id}, ) ``` ```go Go theme={null} _, err := client.Auth.Connections.Update(ctx, auth.ID, kernel.AuthConnectionUpdateParams{ ManagedAuthUpdateRequest: kernel.ManagedAuthUpdateRequestParam{ Proxy: kernel.ManagedAuthUpdateRequestProxyParam{ ID: kernel.String(newProxy.ID), }, }, }) if err != nil { panic(err) } ``` You can also override the connection's proxy for a single login by passing `proxy` on `.login()` — useful when you want to try a one-off egress without changing the connection-wide default (which would also affect subsequent health checks and reauths). ```typescript TypeScript theme={null} const login = await kernel.auth.connections.login(auth.id, { proxy: { id: oneOffProxy.id }, }); ``` ```python Python theme={null} login = await kernel.auth.connections.login( auth.id, proxy={"id": one_off_proxy.id}, ) ``` ```go Go theme={null} login, err := client.Auth.Connections.Login(ctx, auth.ID, kernel.AuthConnectionLoginParams{ Proxy: kernel.AuthConnectionLoginParamsProxy{ ID: kernel.String(oneOffProxy.ID), }, }) if err != nil { panic(err) } _ = login ``` ## Record Sessions for Debugging Set `record_session: true` to capture a [replay](/docs/browsers/replays) of every browser session tied to the connection — initial logins, background health checks, and automatic re-authentications. The entire browser session is recorded. ```typescript TypeScript theme={null} const auth = await kernel.auth.connections.create({ domain: 'example.com', profile_name: 'my-profile', record_session: true, }); ``` ```python Python theme={null} auth = await kernel.auth.connections.create( domain="example.com", profile_name="my-profile", record_session=True, ) ``` ```go Go theme={null} auth, err := client.Auth.Connections.New(ctx, kernel.AuthConnectionNewParams{ ManagedAuthCreateRequest: kernel.ManagedAuthCreateRequestParam{ Domain: "example.com", ProfileName: "my-profile", RecordSession: kernel.Bool(true), }, }) if err != nil { panic(err) } _ = auth ``` You can also override the connection default for a single login by passing `record_session` on `.login()` — useful for one-off debugging on a specific login attempt without flipping the connection-wide flag (which would also record subsequent health checks and reauths). ```typescript TypeScript theme={null} const login = await kernel.auth.connections.login(auth.id, { record_session: true, }); ``` ```python Python theme={null} login = await kernel.auth.connections.login( auth.id, record_session=True, ) ``` ```go Go theme={null} login, err := client.Auth.Connections.Login(ctx, auth.ID, kernel.AuthConnectionLoginParams{ RecordSession: kernel.Bool(true), }) if err != nil { panic(err) } _ = login ``` Managed auth recordings are subject to the same retention rules as other session replay recordings. Each managed auth session row stores its own `replay_id` for the recording captured during that session. ## Post-Login URL After successful authentication, `post_login_url` will be set to the page where the login landed. Use this to start your automation from the right place: ```typescript TypeScript theme={null} const managedAuth = await kernel.auth.connections.retrieve(auth.id); if (managedAuth.post_login_url) { await page.goto(managedAuth.post_login_url); // Start automation from the dashboard/home page } ``` ```python Python theme={null} managed_auth = await kernel.auth.connections.retrieve(auth.id) if managed_auth.post_login_url: await page.goto(managed_auth.post_login_url) # Start automation from the dashboard/home page ``` ```go Go theme={null} managedAuth, err := client.Auth.Connections.Get(ctx, auth.ID) if err != nil { panic(err) } if managedAuth.PostLoginURL != "" { _, err := client.Browsers.Playwright.Execute(ctx, browser.SessionID, kernel.BrowserPlaywrightExecuteParams{ Code: fmt.Sprintf(`await page.goto(%q);`, managedAuth.PostLoginURL), }) if err != nil { panic(err) } // Start automation from the dashboard/home page } ``` ## Updating a Connection After creating a connection, you can update its configuration with `auth.connections.update`: | Field | Description | | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | `login_url` | Override the login page URL | | `credential` | Update the linked credential | | `allowed_domains` | Update allowed redirect domains | | `health_check_interval` | Seconds between health checks (minimum varies by plan) | | `health_checks` | Whether periodic health checks run for this connection | | `auto_reauth` | Whether a failed scheduled health check is allowed to attempt automatic re-authentication | | `save_credentials` | Whether to save credentials on successful login | | `record_session` | Record a [replay](/docs/browsers/replays) of every auth browser session for this connection (logins, health checks, and reauths) | | `proxy` | Pin login, health-check, and reauth sessions to a proxy. Takes effect on the next health check or reauth | Only the fields you include are updated—everything else stays the same. Changes to `health_check_interval`, `health_checks`, `auto_reauth`, and `proxy` take effect immediately on the running connection. ```typescript TypeScript theme={null} await kernel.auth.connections.update(auth.id, { login_url: 'https://example.com/new-login', health_check_interval: 1800, save_credentials: true, }); ``` ```python Python theme={null} await kernel.auth.connections.update( auth.id, login_url="https://example.com/new-login", health_check_interval=1800, save_credentials=True, ) ``` ```go Go theme={null} _, err := client.Auth.Connections.Update(ctx, auth.ID, kernel.AuthConnectionUpdateParams{ ManagedAuthUpdateRequest: kernel.ManagedAuthUpdateRequestParam{ LoginURL: kernel.String("https://example.com/new-login"), HealthCheckInterval: kernel.Int(1800), SaveCredentials: kernel.Bool(true), }, }) if err != nil { panic(err) } ``` # Connection Lifecycle Source: https://kernel.sh/docs/auth/connection-lifecycle How connections stay authenticated, and what to do when one breaks Once a Managed Auth connection is `AUTHENTICATED`, Kernel runs periodic health checks. For eligible flows, Kernel can automatically reauthenticate the session to keep it valid. This page covers the runtime lifecycle, configuration options, and recovery steps. ## The lifecycle After the initial login, every connection moves through this loop: On a configurable cadence, Kernel spins up a browser with the profile and verifies the session. A check reaches one of three conclusions: still logged in, definitely logged out, or **inconclusive** — the page didn't prove either way. A check needs concrete evidence to conclude. If the site returns a challenge page, times out, partially loads, or shows something ambiguous, the result is inconclusive and the connection is left exactly as it was. Nothing happens until the next check. If the check finds the session **definitely** expired and the connection's `can_reauth` is `true`, Kernel runs the saved credential-based login flow in the background. A successful login resets the loop. An inconclusive check never triggers reauth. Kernel would rather check again on the next cycle than log in again unnecessarily — a spurious login can trip risk checks on the site, prompt a device-verification email, or invalidate a working session. If automatic reauthentication isn't possible — for example, credentials are missing, human input is required, or the login keeps failing — the connection's `status` changes to `NEEDS_AUTH`. Start a new login session to continue. ## Cadence Health checks run on a configurable interval. Your plan sets the minimum: | Plan | Minimum interval | | ---------- | ---------------------------------------- | | Hobbyist | 1 hour | | Start-Up | 20 minutes | | Enterprise | Fully configurable (as low as 5 minutes) | You can raise the interval above your plan's minimum, but not below it. Update it with `health_check_interval` (in seconds) — changes take effect immediately, so the next check uses the new value: ```typescript TypeScript theme={null} await kernel.auth.connections.update(auth.id, { health_check_interval: 1800, // 30 minutes }); ``` ```python Python theme={null} await kernel.auth.connections.update( auth.id, health_check_interval=1800, # 30 minutes ) ``` ```go Go theme={null} _, err := client.Auth.Connections.Update(ctx, auth.ID, kernel.AuthConnectionUpdateParams{ ManagedAuthUpdateRequest: kernel.ManagedAuthUpdateRequestParam{ HealthCheckInterval: kernel.Int(1800), // 30 minutes }, }) if err != nil { panic(err) } ``` ### Sessions that expire faster than the interval For an eligible connection, Kernel continues reauthenticating when every health check finds the session expired. A successful login resets the automatic reauthentication state, so a site whose session TTL is shorter than your health check interval is reauthenticated on every cycle. If you're seeing the connection flip to `NEEDS_AUTH` frequently and want shorter detection windows, lower `health_check_interval` toward your plan's minimum. ## Can this connection auto-reauth? Check the `can_reauth` boolean on a connection. It describes eligibility to attempt reauthentication, not whether the next login will succeed. It's `true` only when all of these conditions hold: 1. **A previous login succeeded** — Kernel has recorded the result of a completed login. 2. **A credential is linked** — It's stored in Kernel or sourced from [1Password](/docs/integrations/1password). 3. **No blocking requirement is recorded** — Kernel hasn't recorded a requirement that prevents an automatic attempt. Use `can_reauth_reason` to see why the connection is or isn't eligible. See [Get auth connection](https://kernel.sh/docs/api-reference/managed-auth/get-auth-connection) for the possible values. `can_reauth` reflects what Kernel knows about the previous login. A site can introduce a new requirement during a later attempt. If Kernel needs input, a choice, or approval, the connection moves to `NEEDS_AUTH` so you can start a new login. ### Flows that need input, a choice, or approval After a successful login, Kernel saves the login flow. If a later attempt needs input, a choice, or approval, the connection moves to `NEEDS_AUTH` instead of guessing how to proceed. You can handle these flows in two ways: * **Switch to TOTP** — If the site supports authenticator apps, add a `totp_secret` to your credential. Codes are generated on demand, so the flow no longer needs external action. If a code expires before the site accepts it, Kernel retries with a fresh one. * **Trigger manual re-auth** — Start a new login session and route the user through the [Hosted UI](/docs/auth/hosted-ui) or [Programmatic](/docs/auth/programmatic) flow. ## Triggering re-auth manually Call `.login()` on any connection to trigger authentication immediately, without waiting for the next scheduled health check. If the profile is already logged in, it returns quickly without starting a new flow. If the connection needs auth, it starts a new login session. This is useful when your workflow needs to ensure a connection is authenticated *right now*: ```typescript TypeScript theme={null} const state = await kernel.auth.connections.retrieve(auth.id); if (state.status === 'NEEDS_AUTH') { const login = await kernel.auth.connections.login(auth.id); // Handle login flow as usual } ``` ```python Python theme={null} state = await kernel.auth.connections.retrieve(auth.id) if state.status == "NEEDS_AUTH": login = await kernel.auth.connections.login(auth.id) # Handle login flow as usual ``` ```go Go theme={null} state, err := client.Auth.Connections.Get(ctx, auth.ID) if err != nil { panic(err) } if state.Status == kernel.ManagedAuthStatusNeedsAuth { login, err := client.Auth.Connections.Login(ctx, auth.ID, kernel.AuthConnectionLoginParams{}) if err != nil { panic(err) } _ = login // Handle login flow as usual } ``` ## When a login fails If a login attempt fails — whether triggered by a health check, an auto-reauth, or a manual `.login()` — the flow is marked `FAILED` and the event includes `error_code` and `error_message`. How much Kernel retries depends on the failure. A transient site problem (a 5xx page, a maintenance screen) is retried once against the login page before giving up. A conclusive rejection by the site — wrong credentials, a locked account, an unsupported method — is **not** retried, because retrying would burn attempts against a lockout or fail identically. Common codes: | Code | Meaning | Retried | | -------------------------- | --------------------------------------------------------------------------------- | --------------------------- | | `credentials_invalid` | The stored or submitted username or password was rejected by the site. | No | | `totp_code_rejected` | The site rejected an authenticator code Kernel submitted. | No; retry with a fresh code | | `totp_required` | Reauth requires an authenticator code that Kernel couldn't provide automatically. | No | | `sms_code_required` | Reauth requires a code sent by SMS. | No | | `email_code_required` | Reauth requires a code sent by email. | No | | `account_choice_required` | Reauth requires a person to select an account or identity. | No | | `customer_input_required` | Reauth requires another field or choice from a person. | No | | `external_action_required` | Reauth requires a push approval, link, or another action outside the browser. | No | | `account_locked` | The site locked or suspended the account. | No | | `unsupported_auth_method` | The site required a method Kernel doesn't currently support (e.g. passkeys). | No | | `rate_limited` | The site rate-limited the login attempt. | No; retry later | | `website_error` | The site returned an error, maintenance, or unavailable page. | Once | | `bot_detected` | The login page blocked the session as automated. | No | | `captcha_blocked` | A CAPTCHA was presented and couldn't be solved. | No | See the [API reference](https://kernel.sh/docs/api-reference/managed-auth/start-login-flow) for the full list. ### Recovering * **`credentials_invalid`** — Update the linked [credential](/docs/auth/credentials) and call `.login()` to re-run the flow. When the site identifies which field it rejected during an interactive login, Kernel asks for a corrected value in place — see [replacing a rejected credential](/docs/auth/programmatic#replacing-a-rejected-credential). * **`totp_code_rejected`** — Retry with a code from a new TOTP window. If independently generated codes keep failing, reconnect the account and update its TOTP secret. One rejected code does not prove that the saved secret is stale. * **`totp_required` / `sms_code_required` / `email_code_required`** — Start an interactive login and provide the requested code. Add a TOTP secret to the linked credential to make future authenticator-code challenges automatic. * **`account_choice_required` / `customer_input_required` / `external_action_required`** — Start an interactive login and complete the choice, field, or external approval. Kernel does not guess an identity or trigger notification-producing steps during unattended reauth. * **`account_locked`** — Unlock the account with the site directly. Calling `.login()` again before that will not help and may extend the lockout. * **`rate_limited`** — Wait for the site's retry window before starting another login. * **`bot_detected` / `captcha_blocked`** — Pin the connection to a cleaner [proxy](/docs/auth/configuration#custom-proxy) (ISP or custom). For aggressive sites, also enable stealth and review the [bot detection guide](/docs/browsers/bot-detection/overview). * **`unsupported_auth_method`** — Switch the account to a supported sign-in method (e.g. password + TOTP instead of a passkey) and re-link the credential. * **`website_error`** — Usually the site, not the connection. Retry later; if it persists, confirm the connection's `login_url` still points at a working login page. ## Debugging a flaky connection Two tools handle most investigations: 1. **Dashboard live view** — The **Browser Sessions** tab in the Kernel dashboard shows every auth browser session (logins, health checks, reauths) with a live view. Watch a session in real time to see exactly where it's getting stuck. 2. **Session recordings** — To record only the next single login attempt without recording subsequent health checks and reauths, pass `record_session: true` on `.login()`: ```typescript TypeScript theme={null} const login = await kernel.auth.connections.login(auth.id, { record_session: true, }); ``` ```python Python theme={null} login = await kernel.auth.connections.login( auth.id, record_session=True, ) ``` ```go Go theme={null} login, err := client.Auth.Connections.Login(ctx, auth.ID, kernel.AuthConnectionLoginParams{ RecordSession: kernel.Bool(true), }) if err != nil { panic(err) } _ = login ``` To record every auth session on the connection (logins, health checks, and reauths), set `record_session: true` connection-wide — see [Record Sessions for Debugging](/docs/auth/configuration#record-sessions-for-debugging). ## See also * [Connection Configuration](/docs/auth/configuration) — `health_check_interval`, `proxy`, `record_session`, and other shared options * [Credentials](/docs/auth/credentials) — what gets stored and how it powers auto-reauth * [FAQ](/docs/auth/faq) — quick answers to common questions # Credentials Source: https://kernel.sh/docs/auth/credentials Use stored credentials for login and automatic reauthentication Credentials let you store login information securely. Kernel can automatically authenticate credential-only flows and attempts to provide TOTP codes when needed. **There are three ways to provide credentials:** * **Automatically save during login** — Capture credentials directly from the user when they log in via [Hosted UI](/docs/auth/hosted-ui) or [Programmatic](/docs/auth/programmatic) * **Pre-store in Kernel** — Create credentials before login for supported headless authentication flows * **Connect 1Password** — Use credentials from your existing 1Password vaults Connect your 1Password vaults to automatically use existing credentials with Managed Auth. Credentials are automatically matched by domain. ## Save credentials during login By default, Kernel saves durable credential fields entered during login so they can be used for eligible reauthentication attempts. No extra parameters are needed: ```typescript TypeScript theme={null} const login = await kernel.auth.connections.login(auth.id); ``` ```python Python theme={null} login = await kernel.auth.connections.login(auth.id) ``` ```go Go theme={null} login, err := client.Auth.Connections.Login(ctx, auth.ID, kernel.AuthConnectionLoginParams{}) if err != nil { panic(err) } _ = login ``` Once saved, the browser profile reuses its authenticated session until the site expires it. For supported credential-based flows, Kernel can then reauthenticate with the stored values. Credentials are updated after every successful login. Submitted one-time codes aren't saved; Kernel generates TOTP codes from a stored `totp_secret`. To opt out of credential saving, set `save_credentials: false` when creating the connection: ```typescript TypeScript theme={null} const auth = await kernel.auth.connections.create({ domain: 'example.com', profile_name: 'my-profile', save_credentials: false, }); ``` ```python Python theme={null} auth = await kernel.auth.connections.create( domain="example.com", profile_name="my-profile", save_credentials=False, ) ``` ```go Go theme={null} auth, err := client.Auth.Connections.New(ctx, kernel.AuthConnectionNewParams{ ManagedAuthCreateRequest: kernel.ManagedAuthCreateRequestParam{ Domain: "example.com", ProfileName: "my-profile", SaveCredentials: kernel.Bool(false), }, }) if err != nil { panic(err) } _ = auth ``` ## Pre-store credentials For credential-based flows that you want to run without user input, create credentials upfront: ```typescript TypeScript theme={null} const credential = await kernel.credentials.create({ name: 'my-netflix-login', domain: 'netflix.com', values: { email: 'user@netflix.com', password: 'secretpassword123', }, }); ``` ```python Python theme={null} credential = await kernel.credentials.create( name="my-netflix-login", domain="netflix.com", values={ "email": "user@netflix.com", "password": "secretpassword123", }, ) ``` ```go Go theme={null} credential, err := client.Credentials.New(ctx, kernel.CredentialNewParams{ CreateCredentialRequest: kernel.CreateCredentialRequestParam{ Name: "my-netflix-login", Domain: "netflix.com", Values: map[string]string{ "email": "user@netflix.com", "password": "secretpassword123", }, }, }) if err != nil { panic(err) } _ = credential ``` Then link the credential when creating a connection: ```typescript TypeScript theme={null} const auth = await kernel.auth.connections.create({ domain: 'netflix.com', profile_name: 'my-profile', credential: { name: credential.name }, }); // Start login with stored credentials const login = await kernel.auth.connections.login(auth.id); ``` ```python Python theme={null} auth = await kernel.auth.connections.create( domain="netflix.com", profile_name="my-profile", credential={"name": credential.name}, ) # Start login with stored credentials login = await kernel.auth.connections.login(auth.id) ``` ```go Go theme={null} auth, err := client.Auth.Connections.New(ctx, kernel.AuthConnectionNewParams{ ManagedAuthCreateRequest: kernel.ManagedAuthCreateRequestParam{ Domain: "netflix.com", ProfileName: "my-profile", Credential: kernel.ManagedAuthCreateRequestCredentialParam{ Name: kernel.String(credential.Name), }, }, }) if err != nil { panic(err) } // Start login with stored credentials login, err := client.Auth.Connections.Login(ctx, auth.ID, kernel.AuthConnectionLoginParams{}) if err != nil { panic(err) } _ = login ``` ### 2FA with TOTP For sites with authenticator app 2FA, include `totp_secret` so Kernel can generate a fresh code during automatic login and reauthentication: ```typescript TypeScript theme={null} const credential = await kernel.credentials.create({ name: 'my-login', domain: 'github.com', values: { username: 'my-username', password: 'my-password', }, totp_secret: 'JBSWY3DPEHPK3PXP', // From authenticator app setup }); ``` ```python Python theme={null} credential = await kernel.credentials.create( name="my-login", domain="github.com", values={ "username": "my-username", "password": "my-password", }, totp_secret="JBSWY3DPEHPK3PXP", # From authenticator app setup ) ``` ```go Go theme={null} credential, err := client.Credentials.New(ctx, kernel.CredentialNewParams{ CreateCredentialRequest: kernel.CreateCredentialRequestParam{ Name: "my-login", Domain: "github.com", Values: map[string]string{ "username": "my-username", "password": "my-password", }, TotpSecret: kernel.String("JBSWY3DPEHPK3PXP"), // From authenticator app setup }, }) if err != nil { panic(err) } _ = credential ``` ### SSO / OAuth For sites with "Sign in with Google/GitHub/Microsoft", set `sso_provider` so Kernel can select the matching SSO route. Automatic completion depends on the provider's login requirements. Common SSO provider domains (Google, Microsoft, Okta, Auth0, GitHub, etc.) are allowed by default, so you don't need to add them to `allowed_domains`: ```typescript TypeScript theme={null} const credential = await kernel.credentials.create({ name: 'my-google-login', domain: 'accounts.google.com', sso_provider: 'google', values: { email: 'user@gmail.com', password: 'password', }, }); const auth = await kernel.auth.connections.create({ domain: 'target-site.com', profile_name: 'my-profile', credential: { name: credential.name }, }); ``` ```python Python theme={null} credential = await kernel.credentials.create( name="my-google-login", domain="accounts.google.com", sso_provider="google", values={ "email": "user@gmail.com", "password": "password", }, ) auth = await kernel.auth.connections.create( domain="target-site.com", profile_name="my-profile", credential={"name": credential.name}, ) ``` ```go Go theme={null} credential, err := client.Credentials.New(ctx, kernel.CredentialNewParams{ CreateCredentialRequest: kernel.CreateCredentialRequestParam{ Name: "my-google-login", Domain: "accounts.google.com", SSOProvider: kernel.String("google"), Values: map[string]string{ "email": "user@gmail.com", "password": "password", }, }, }) if err != nil { panic(err) } auth, err := client.Auth.Connections.New(ctx, kernel.AuthConnectionNewParams{ ManagedAuthCreateRequest: kernel.ManagedAuthCreateRequestParam{ Domain: "target-site.com", ProfileName: "my-profile", Credential: kernel.ManagedAuthCreateRequestCredentialParam{ Name: kernel.String(credential.Name), }, }, }) if err != nil { panic(err) } _ = auth ``` ## Partial Credentials Credentials don't need to contain every field required by the login form. You can store what you have and collect the necessary fields from the user. `auth.connections.login()` pauses for missing values. As an example, the below credential has email + TOTP secret stored (and automatically handled), but no password. The password is dynamically collected from the user using Kernel's Hosted UI or your Programmatic flow: ```typescript TypeScript theme={null} const credential = await kernel.credentials.create({ name: 'my-login', domain: 'example.com', values: { email: 'user@example.com' }, // No password totp_secret: 'JBSWY3DPEHPK3PXP', }); const auth = await kernel.auth.connections.create({ domain: 'example.com', profile_name: 'my-profile', credential: { name: credential.name }, }); const login = await kernel.auth.connections.login(auth.id); // Stream state changes and submit the missing password const authEvents = await kernel.auth.connections.follow(auth.id); for await (const event of authEvents) { const passwordField = event.fields?.find(field => field.ref === 'password'); if ( event.event === 'managed_auth_state' && event.flow_step === 'AWAITING_INPUT' && event.interaction_id && passwordField ) { // Only password is pending; email is filled from the stored credential. await kernel.auth.connections.submit(auth.id, { interaction_id: event.interaction_id, field_values: { [passwordField.id]: 'user-provided-password' }, }); } } // TOTP auto-submitted from credential → SUCCESS ``` ```python Python theme={null} credential = await kernel.credentials.create( name="my-login", domain="example.com", values={"email": "user@example.com"}, # No password totp_secret="JBSWY3DPEHPK3PXP", ) auth = await kernel.auth.connections.create( domain="example.com", profile_name="my-profile", credential={"name": credential.name}, ) login = await kernel.auth.connections.login(auth.id) # Stream state changes and submit the missing password auth_events = await kernel.auth.connections.follow(auth.id) async for event in auth_events: password_field = next( (field for field in (event.fields or []) if field.ref == "password"), None, ) if ( event.event == "managed_auth_state" and event.flow_step == "AWAITING_INPUT" and event.interaction_id and password_field ): # Only password is pending; email is filled from the stored credential. await kernel.auth.connections.submit( auth.id, interaction_id=event.interaction_id, field_values={password_field.id: "user-provided-password"}, ) # TOTP auto-submitted from credential → SUCCESS ``` ```go Go theme={null} credential, err := client.Credentials.New(ctx, kernel.CredentialNewParams{ CreateCredentialRequest: kernel.CreateCredentialRequestParam{ Name: "my-login", Domain: "example.com", Values: map[string]string{ "email": "user@example.com", // No password }, TotpSecret: kernel.String("JBSWY3DPEHPK3PXP"), }, }) if err != nil { panic(err) } auth, err := client.Auth.Connections.New(ctx, kernel.AuthConnectionNewParams{ ManagedAuthCreateRequest: kernel.ManagedAuthCreateRequestParam{ Domain: "example.com", ProfileName: "my-profile", Credential: kernel.ManagedAuthCreateRequestCredentialParam{ Name: kernel.String(credential.Name), }, }, }) if err != nil { panic(err) } login, err := client.Auth.Connections.Login(ctx, auth.ID, kernel.AuthConnectionLoginParams{}) if err != nil { panic(err) } _ = login // Stream state changes and submit the missing password authEvents := client.Auth.Connections.FollowStreaming(ctx, auth.ID) for authEvents.Next() { event := authEvents.Current() if event.Event != "managed_auth_state" || event.FlowStep != "AWAITING_INPUT" || event.InteractionID == "" { continue } for _, field := range event.Fields { if field.Ref != "password" { continue } // Only password is pending; email is filled from the stored credential. _, err := client.Auth.Connections.Submit(ctx, auth.ID, kernel.AuthConnectionSubmitParams{ SubmitFieldsRequest: kernel.SubmitFieldsRequestParam{ InteractionID: kernel.String(event.InteractionID), FieldValues: map[string]string{ field.ID: "user-provided-password", }, }, }) if err != nil { panic(err) } break } } if err := authEvents.Err(); err != nil { panic(err) } // TOTP auto-submitted from credential → SUCCESS ``` This is useful when you want to: * Store TOTP secrets but have users enter their password each time * Pre-fill username/email but collect password at runtime * Merge user-provided values into an existing credential automatically on successful login ## Security | Feature | Description | | ---------------------- | ---------------------------------------------------- | | **Encrypted at rest** | Values encrypted using per-organization keys | | **Write-only** | Values cannot be retrieved via API after creation | | **Never logged** | Values are never written to logs | | **Never shared** | Values are never passed to LLMs | | **Isolated execution** | Authentication runs in isolated browser environments | ## Notes * The `values` object is flexible and can be used to store whatever fields the login form needs (`email`, `username`, `company_id`, etc.) * Deleting a credential unlinks it from associated connections so they can no longer auto-authenticate * Use one credential per account. We recommend creating separate credentials for different user accounts # FAQ Source: https://kernel.sh/docs/auth/faq ## How does automatic re-authentication work? When you link credentials to a connection, Kernel runs periodic health checks and can reauthenticate supported credential-based flows in the background. This includes TOTP when Kernel can provide the authenticator code. See [Connection Lifecycle](/docs/auth/connection-lifecycle) for the full lifecycle, cadence options, and `can_reauth` rules. ## What are auth choices? Auth choices are visible routes a site presents during login, including mfa methods, sso providers, account pickers, and organization selectors. They appear in the canonical `choices` array. Submit the exact returned id with `interaction_id` and `selected_choice_id`. See the [programmatic flow guide](/docs/auth/programmatic#choices) for examples. ## Which authentication methods are supported? Managed Auth supports common credential, SSO, and multi-step login flows. Automatic reauthentication uses stored credentials and attempts to provide TOTP codes when needed. Passkey-only authentication isn't currently supported. If a site's SSO provider requires a passkey, the login returns `unsupported_auth_method`. Switch the account to a supported sign-in method, such as password and TOTP, then start a new login. ## What happens if login fails? Kernel surfaces an error code (`credentials_invalid`, `account_locked`, `bot_detected`, `captcha_blocked`, etc.). Transient site failures are retried; a conclusive rejection by the site isn't, so Kernel doesn't burn attempts against a locked account or resubmit credentials the site already refused. See [Connection Lifecycle](/docs/auth/connection-lifecycle#when-a-login-fails) for the full list and recovery steps. ## Can I use Managed Auth with any website? Managed Auth covers common login flows across a broad range of websites. Site-specific authentication and bot detection can require additional configuration. See [what Managed Auth supports](/docs/auth/overview#why-managed-auth) and test your target flow. ## Is Managed Auth available during a trial? Yes. Managed Auth and browser profiles are available during your trial period with the same capabilities as the plan you're trialing. ## How do I re-authenticate a connection before the next health check? Call `.login()` on the connection to trigger auth immediately. See [Triggering re-auth manually](/docs/auth/connection-lifecycle#triggering-re-auth-manually) for the pattern. ## What types of flows does Managed Auth support? Managed Auth navigates login pages, enters stored credentials, follows SSO redirects, guides users through additional authentication steps, and saves the resulting browser session. For post-login work like form filling, sign-ups, or other workflows, use [Kernel's browser automation](/docs/introduction/control) directly. ## How do I debug a managed auth session? Use the **Browser Sessions** tab in the dashboard for live view, or set `record_session: true` to capture replays of every auth browser session. See [Debugging a flaky connection](/docs/auth/connection-lifecycle#debugging-a-flaky-connection) for details. ## Can I attach multiple auth connections to one profile? Yes. A profile can have any number of auth connections, each for a different domain. When you create a browser with that profile, it loads the saved authentication state for every connected domain. This is useful for two common patterns: * **Multi-site workflows** — Your agent visits multiple sites in a single run (e.g., reads email in Gmail, posts a summary in Slack, and updates a CRM). Attach one auth connection per site to a single profile, and each browser loads the saved authentication state for all of them. * **User-to-profile mapping** — Each end user on your platform gets one profile. All of that user's accounts (Gmail, LinkedIn, GitHub, etc.) are auth connections on their profile. When the user triggers a workflow, launch a browser with their profile. See [Profiles — Multiple auth connections per profile](/docs/auth/profiles#multiple-auth-connections-per-profile) for code examples. ## How is Managed Auth billed? Managed Auth is included on all plans with no per-connection fees. It uses browser sessions for login, health checks, and eligible reauthentication attempts. These count toward your browser usage like any other browser session. Auth sessions are fast (typically 5-30 seconds each). Kernel monitors session health and can automatically reauthenticate eligible credential-based flows when sessions expire. Most sessions stay valid for days. For example, monitoring 100 auth connections typically costs less than \$5/month in browser usage. See [Pricing & Limits](/docs/info/pricing#managed-auth) for details. # Hosted UI Source: https://kernel.sh/docs/auth/hosted-ui The simplest way to create authenticated browser sessions Collect credentials securely via Kernel's hosted page, then use the authenticated session in your automations. This is the recommended approach for most applications. Use the Hosted UI when: * You need users to provide their credentials * You want the simplest integration with minimal code * You want Kernel to guide users through 2FA and multi-step login flows ## Getting started ### 1. Create a Connection A Managed Auth connection saves a domain's authentication state to a [profile](/docs/auth/profiles) so future browsers can reuse it. You can attach multiple auth connections to the same profile, one per domain. ```typescript TypeScript theme={null} const auth = await kernel.auth.connections.create({ domain: 'linkedin.com', profile_name: 'linkedin-profile', // Name of the profile to associate with the connection }); ``` ```python Python theme={null} auth = await kernel.auth.connections.create( domain="linkedin.com", profile_name="linkedin-profile", # Name of the profile to associate with the connection ) ``` ```go Go theme={null} auth, err := client.Auth.Connections.New(ctx, kernel.AuthConnectionNewParams{ ManagedAuthCreateRequest: kernel.ManagedAuthCreateRequestParam{ Domain: "linkedin.com", ProfileName: "linkedin-profile", // Name of the profile to associate with the connection }, }) if err != nil { panic(err) } _ = auth ``` ### 2. Start a Login Session Start a Managed Auth Session to get the hosted login URL. ```typescript TypeScript theme={null} const login = await kernel.auth.connections.login(auth.id); ``` ```python Python theme={null} login = await kernel.auth.connections.login(auth.id) ``` ```go Go theme={null} login, err := client.Auth.Connections.Login(ctx, auth.ID, kernel.AuthConnectionLoginParams{}) if err != nil { panic(err) } _ = login ``` ### 3. Collect Credentials Send the user to the hosted login page: ```typescript TypeScript theme={null} window.location.href = login.hosted_url; ``` ```python Python theme={null} # Return the URL to your frontend print(f"Redirect to: {login.hosted_url}") ``` ```go Go theme={null} // Return the URL to your frontend fmt.Println("Redirect to:", login.HostedURL) ``` The user will: 1. See the login page for the target website 2. Enter their credentials 3. Complete 2FA or another verification step if needed Kernel can automatically reauthenticate credential-only flows and attempts to provide TOTP codes when needed. ### 4. Stream until completion On your backend, follow the connection's SSE stream until authentication completes: ```typescript TypeScript theme={null} const events = await kernel.auth.connections.follow(auth.id); let finalState; for await (const event of events) { if (event.event === 'managed_auth_state') { finalState = event; } } if (finalState?.flow_status === 'SUCCESS') { console.log('Authentication successful!'); } ``` ```python Python theme={null} events = await kernel.auth.connections.follow(auth.id) final_state = None async for event in events: if event.event == "managed_auth_state": final_state = event if final_state and final_state.flow_status == "SUCCESS": print("Authentication successful!") ``` ```go Go theme={null} events := client.Auth.Connections.FollowStreaming(ctx, auth.ID) authenticated := false for events.Next() { event := events.Current() if event.Event == "managed_auth_state" && event.FlowStatus == "SUCCESS" { authenticated = true } } if err := events.Err(); err != nil { panic(err) } if authenticated { fmt.Println("Authentication successful!") } ``` The SSE stream closes automatically when the flow succeeds, fails, expires, or is canceled. The session expires after 20 minutes if not completed, and the flow times out after 10 minutes of waiting for user input. ### 5. Use the Profile Create browsers with the profile and navigate to the site. The browser loads the authentication state saved during login: ```typescript TypeScript theme={null} const browser = await kernel.browsers.create({ profile: { name: 'linkedin-profile' }, stealth: true, }); // Navigate with the saved authentication state await page.goto('https://linkedin.com'); ``` ```python Python theme={null} browser = await kernel.browsers.create( profile={"name": "linkedin-profile"}, stealth=True, ) # Navigate with the saved authentication state await page.goto("https://linkedin.com") ``` ```go Go theme={null} browser, err := client.Browsers.New(ctx, kernel.BrowserNewParams{ Profile: shared.BrowserProfileParam{ Name: kernel.String("linkedin-profile"), }, Stealth: kernel.Bool(true), }) if err != nil { panic(err) } // Navigate with the saved authentication state _, err = client.Browsers.Playwright.Execute(ctx, browser.SessionID, kernel.BrowserPlaywrightExecuteParams{ Code: `await page.goto("https://linkedin.com");`, }) if err != nil { panic(err) } ``` Managed Auth Connections are generated using Kernel's [stealth](/docs/browsers/bot-detection/stealth) mode. Use `stealth: true` when creating authenticated browser sessions for the best experience. ## Complete Example ```typescript TypeScript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); // Create connection const auth = await kernel.auth.connections.create({ domain: 'doordash.com', profile_name: 'doordash-user-123', }); // Start authentication const login = await kernel.auth.connections.login(auth.id); // Send user to hosted page console.log('Login URL:', login.hosted_url); // Stream state changes until the flow completes const events = await kernel.auth.connections.follow(auth.id); let finalState; for await (const event of events) { if (event.event === 'managed_auth_state') { finalState = event; } } if (finalState?.flow_status === 'SUCCESS') { const browser = await kernel.browsers.create({ profile: { name: 'doordash-user-123' }, stealth: true, }); // Navigate with the saved authentication state await page.goto('https://doordash.com'); } ``` ```python Python theme={null} from kernel import AsyncKernel kernel = AsyncKernel() # Create connection auth = await kernel.auth.connections.create( domain="doordash.com", profile_name="doordash-user-123", ) # Start authentication login = await kernel.auth.connections.login(auth.id) # Send user to hosted page print(f"Login URL: {login.hosted_url}") # Stream state changes until the flow completes events = await kernel.auth.connections.follow(auth.id) final_state = None async for event in events: if event.event == "managed_auth_state": final_state = event if final_state and final_state.flow_status == "SUCCESS": browser = await kernel.browsers.create( profile={"name": "doordash-user-123"}, stealth=True, ) # Navigate with the saved authentication state await page.goto("https://doordash.com") ``` ```go Go theme={null} package main import ( "context" "fmt" "github.com/kernel/kernel-go-sdk" "github.com/kernel/kernel-go-sdk/shared" ) func main() { ctx := context.Background() client := kernel.NewClient() // Create connection auth, err := client.Auth.Connections.New(ctx, kernel.AuthConnectionNewParams{ ManagedAuthCreateRequest: kernel.ManagedAuthCreateRequestParam{ Domain: "doordash.com", ProfileName: "doordash-user-123", }, }) if err != nil { panic(err) } // Start authentication login, err := client.Auth.Connections.Login(ctx, auth.ID, kernel.AuthConnectionLoginParams{}) if err != nil { panic(err) } // Send user to hosted page fmt.Println("Login URL:", login.HostedURL) // Stream state changes until the flow completes events := client.Auth.Connections.FollowStreaming(ctx, auth.ID) authenticated := false for events.Next() { event := events.Current() if event.Event == "managed_auth_state" && event.FlowStatus == "SUCCESS" { authenticated = true } } if err := events.Err(); err != nil { panic(err) } if authenticated { browser, err := client.Browsers.New(ctx, kernel.BrowserNewParams{ Profile: shared.BrowserProfileParam{ Name: kernel.String("doordash-user-123"), }, Stealth: kernel.Bool(true), }) if err != nil { panic(err) } // Navigate with the saved authentication state _, err = client.Browsers.Playwright.Execute(ctx, browser.SessionID, kernel.BrowserPlaywrightExecuteParams{ Code: `await page.goto("https://doordash.com");`, }) if err != nil { panic(err) } } } ``` ## Success / error redirects To redirect the user back to your app once the flow finishes — for example, to auto-close the auth window inside a mobile in-app browser — append `success_url` and/or `error_url` query params to `hosted_url`. These mirror the `onSuccess` / `onError` callbacks exposed by the [React Component](/docs/auth/react). * `success_url` — visited when the session reaches `SUCCESS`. The hosted page appends `profile_name` and `domain` as query params. * `error_url` — visited when the session reaches `FAILED`, `CANCELED`, or `EXPIRED`. The hosted page appends `code` (when present) and `message` as query params. Any scheme is accepted, so mobile integrators can pass a custom scheme (`myapp://auth/done`) to bounce back into the host app. ```typescript TypeScript theme={null} const login = await kernel.auth.connections.login(auth.id); const url = new URL(login.hosted_url); url.searchParams.set("success_url", "https://example.com/connected"); url.searchParams.set("error_url", "https://example.com/auth-failed"); window.location.href = url.toString(); ``` ```python Python theme={null} from urllib.parse import urlencode, urlparse, urlunparse, parse_qsl login = await kernel.auth.connections.login(auth.id) parsed = urlparse(login.hosted_url) query = dict(parse_qsl(parsed.query)) query["success_url"] = "https://example.com/connected" query["error_url"] = "https://example.com/auth-failed" redirect_url = urlunparse(parsed._replace(query=urlencode(query))) ``` ```go Go theme={null} package main import ( "context" "fmt" "net/url" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() login, err := client.Auth.Connections.Login(ctx, "auth_123", kernel.AuthConnectionLoginParams{}) if err != nil { panic(err) } redirectURL, err := url.Parse(login.HostedURL) if err != nil { panic(err) } query := redirectURL.Query() query.Set("success_url", "https://example.com/connected") query.Set("error_url", "https://example.com/auth-failed") redirectURL.RawQuery = query.Encode() fmt.Println(redirectURL.String()) } ``` The hosted page redirects to whatever URL you pass. Only set these from your own trusted backend — never let an end user supply them directly. ## Connection Configuration Connection-level options — custom login URL, SSO/OAuth, custom proxy, session recording, post-login URL, and updates — apply equally to all integration flows and are documented in [Connection Configuration](/docs/auth/configuration). # Overview Source: https://kernel.sh/docs/auth/overview Maintain authenticated browser sessions for agents Managed Auth creates and maintains authenticated browser sessions for your AI agents. Store credentials once, and Kernel can automatically reauthenticate supported login flows when needed. When you launch Kernel browsers with Managed Auth connections, your agent can start logged in and ready to go. ## How It Works A **Managed Auth Connection** attaches a domain's authentication state to a browser [profile](/docs/auth/profiles) so future browsers can reuse it. A single profile can have multiple auth connections, one per domain. ```typescript TypeScript theme={null} const auth = await kernel.auth.connections.create({ domain: 'netflix.com', profile_name: 'netflix-user-123', }); ``` ```python Python theme={null} auth = await kernel.auth.connections.create( domain="netflix.com", profile_name="netflix-user-123", ) ``` ```go Go theme={null} auth, err := client.Auth.Connections.New(ctx, kernel.AuthConnectionNewParams{ ManagedAuthCreateRequest: kernel.ManagedAuthCreateRequestParam{ Domain: "netflix.com", ProfileName: "netflix-user-123", }, }) if err != nil { panic(err) } _ = auth ``` A **Managed Auth Session** is the corresponding login flow for the specified connection. Users provide credentials via a Kernel-hosted page or your own UI. Specify a [Credential](/docs/auth/credentials) to enable automatic reauthentication for supported credential-based flows. ```typescript TypeScript theme={null} const login = await kernel.auth.connections.login(auth.id); // Send user to login page console.log('Login URL:', login.hosted_url); // Stream state changes until the flow completes const events = await kernel.auth.connections.follow(auth.id); let finalState; for await (const event of events) { if (event.event === 'managed_auth_state') { finalState = event; } } if (finalState?.flow_status === 'SUCCESS') { console.log('Authenticated!'); } ``` ```python Python theme={null} login = await kernel.auth.connections.login(auth.id) # Send user to login page print(f"Login URL: {login.hosted_url}") # Stream state changes until the flow completes events = await kernel.auth.connections.follow(auth.id) final_state = None async for event in events: if event.event == "managed_auth_state": final_state = event if final_state and final_state.flow_status == "SUCCESS": print("Authenticated!") ``` ```go Go theme={null} login, err := client.Auth.Connections.Login(ctx, auth.ID, kernel.AuthConnectionLoginParams{}) if err != nil { panic(err) } // Send user to login page fmt.Println("Login URL:", login.HostedURL) // Stream state changes until the flow completes events := client.Auth.Connections.FollowStreaming(ctx, auth.ID) authenticated := false for events.Next() { event := events.Current() if event.Event == "managed_auth_state" && event.FlowStatus == "SUCCESS" { authenticated = true } } if err := events.Err(); err != nil { panic(err) } if authenticated { fmt.Println("Authenticated!") } ``` Once the auth connection completes, the authenticated session is saved to the browser [profile](/docs/auth/profiles) specified in step 1. You can attach additional auth connections to the same profile for other domains. When you create a browser with the profile, it loads the saved authentication state for every connected domain. ```typescript TypeScript theme={null} const browser = await kernel.browsers.create({ profile: { name: 'netflix-user-123' }, stealth: true, }); // Navigate with the saved authentication state await page.goto('https://netflix.com'); ``` ```python Python theme={null} browser = await kernel.browsers.create( profile={"name": "netflix-user-123"}, stealth=True, ) # Navigate with the saved authentication state await page.goto("https://netflix.com") ``` ```go Go theme={null} browser, err := client.Browsers.New(ctx, kernel.BrowserNewParams{ Profile: shared.BrowserProfileParam{ Name: kernel.String("netflix-user-123"), }, Stealth: kernel.Bool(true), }) if err != nil { panic(err) } _ = browser // Navigate with the saved authentication state _, err = client.Browsers.Playwright.Execute(ctx, browser.SessionID, kernel.BrowserPlaywrightExecuteParams{ Code: `await page.goto("https://netflix.com");`, }) if err != nil { panic(err) } ``` The steps above are the integration loop you wire up once per connection. After the initial login, Kernel monitors the connection with periodic health checks and can automatically reauthenticate eligible flows. See [Connection Lifecycle](/docs/auth/connection-lifecycle) for the runtime behavior and configuration options. ## Choose Your Integration **Start here** - Simplest integration Redirect users to Kernel's hosted page. Add features incrementally: save credentials for eligible automatic reauthentication, set custom login URLs, and configure SSO. **Embed in your app** - Drop-in component Mount `` on a route in your own app. Same flow as Hosted UI, rendered on your origin and trivial to restyle to match your brand. **Full control** - Custom UI or headless Build your own credential collection. Handle login fields, SSO buttons, MFA selection, and external actions (push notifications, security keys). ## Why Managed Auth? Managed Auth runs **login flows** by navigating login pages, filling credentials, following SSO redirects, and guiding users through additional authentication steps. It saves the resulting session state to a reusable profile. The most valuable workflows live behind logins. Managed Auth provides: * **Broad site coverage** - Login pages are discovered and handled across common website login flows * **SSO/OAuth support** - Kernel follows common SSO redirects. Common provider domains are allowed by default; add custom provider domains to `allowed_domains` * **2FA/OTP handling** - Kernel attempts to provide TOTP codes automatically; interactive login can collect other verification steps * **Post-login URL** - Get the URL where login landed (`post_login_url`) so you can start automations from the right page * **Session monitoring** - [Periodic health checks](/docs/auth/connection-lifecycle) and automatic reauthentication for eligible credential-based flows * **Secure by default** - Credentials are encrypted at rest and never exposed in API responses or passed to LLMs ## Security | Feature | Description | | -------------------------- | -------------------------------------------------- | | **Encrypted credentials** | Values encrypted with per-organization keys | | **No credential exposure** | Never returned in API responses or passed to LLMs | | **Encrypted profiles** | Browser session state encrypted end-to-end | | **Isolated execution** | Each login runs in an isolated browser environment | # Profiles Source: https://kernel.sh/docs/auth/profiles The primitive to persist and reuse browser session state (cookies, local storage) across browsers Profiles let you capture browser state created during a session (cookies and local storage) and reuse it in later sessions. This is the primitive to instantiate [authenticated browsers](/docs/auth/overview). ## 1. Create a profile When you create a [Managed Auth connection](/docs/auth/overview), it is attached to a profile. A single profile can hold multiple auth connections, one per domain, so a browser launched with that profile loads the saved authentication state for each connection. You can also use profiles without Managed Auth. The first step in using profiles is to create one, optionally giving it a meaningful `name` that is unique within your [project](/docs/info/projects). ```typescript Typescript/Javascript theme={null} import Kernel, { ConflictError } from '@onkernel/sdk'; const kernel = new Kernel(); try { await kernel.profiles.create({ name: 'profiles-demo' }); } catch (err) { if (err instanceof ConflictError) { // Profile already exists } else { throw err; } } ``` ```python Python theme={null} from kernel import Kernel, ConflictError kernel = Kernel() try: await kernel.profiles.create(name="profiles-demo") except ConflictError: pass ``` ```go Go theme={null} package main import ( "context" "errors" "net/http" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() _, err := client.Profiles.New(ctx, kernel.ProfileNewParams{ Name: kernel.String("profiles-demo"), }) if err != nil { var apiErr *kernel.Error if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusConflict { // Profile already exists return } panic(err) } } ``` ## Rename a profile Profiles can be renamed without recreating their stored browser state. The new name must be unique within the project. ```typescript TypeScript theme={null} await kernel.profiles.update('profiles-demo', { name: 'checkout-session' }); ``` ```python Python theme={null} kernel.profiles.update("profiles-demo", name="checkout-session") ``` ```go Go theme={null} _, err := client.Profiles.Update(ctx, "profiles-demo", kernel.ProfileUpdateParams{ Name: "checkout-session", }) ``` ## 2. Start a browser session using the profile and save changes After creating the profile, reference it by its `name` or `id` when creating a browser. Set `save_changes` to true to persist any state created during this session back into the profile when the browser is closed. ```typescript Typescript/Javascript theme={null} const kernelBrowser = await kernel.browsers.create({ profile: { name: 'profiles-demo', save_changes: true, }, }); ``` ```python Python theme={null} kernel_browser = await kernel.browsers.create( profile={ "name": "profiles-demo", "save_changes": True, } ) ``` ```go Go theme={null} kernelBrowser, err := client.Browsers.New(ctx, kernel.BrowserNewParams{ Profile: shared.BrowserProfileParam{ Name: kernel.String("profiles-demo"), SaveChanges: kernel.Bool(true), }, }) if err != nil { panic(err) } _ = kernelBrowser ``` ### Prevent concurrent profile writes Saving replaces the profile's entire stored browser state; it doesn't merge cookies or local storage from multiple sessions. If more than one browser uses the same profile with `save_changes: true`, the browser that ends last overwrites the profile with its state. Before starting a writer, call `GET /browsers?status=active&query=` and check `profile_save_changes` to find active sessions that can save to that profile. The `query` parameter can match fields other than the profile ID, so filter the results by the exact `profile.id` too. ```typescript TypeScript theme={null} const profileId = kernelBrowser.profile!.id; const activeWriters = []; for await (const browser of kernel.browsers.list({ status: 'active', query: profileId, })) { if (browser.profile?.id === profileId && browser.profile_save_changes) { activeWriters.push(browser); } } if (activeWriters.length > 0) { throw new Error(`Profile already has an active writer: ${activeWriters[0].session_id}`); } ``` ```python Python theme={null} profile_id = kernel_browser.profile.id active_writers = [ browser for browser in kernel.browsers.list(status="active", query=profile_id) if browser.profile and browser.profile.id == profile_id and browser.profile_save_changes ] if active_writers: raise RuntimeError( f"Profile already has an active writer: {active_writers[0].session_id}" ) ``` ```go Go theme={null} profileID := kernelBrowser.Profile.ID pager := client.Browsers.ListAutoPaging(ctx, kernel.BrowserListParams{ Status: kernel.BrowserListParamsStatusActive, Query: kernel.String(profileID), }) for pager.Next() { browser := pager.Current() if browser.Profile.ID == profileID && browser.ProfileSaveChanges { panic(fmt.Sprintf("profile already has an active writer: %s", browser.SessionID)) } } if err := pager.Err(); err != nil { panic(err) } ``` This check and browser creation are separate requests. If multiple workers can start sessions concurrently, use your own lock or lease around both operations so two workers can't pass the check at the same time. ## 3. Use the browser, then close it to persist the state After using a browser with `save_changes: true`, closing the browser will save cookies and local storage into the profile. Calling `browser.close()` does **not** save the profile state. You **must** explicitly delete the Kernel browser (or let the browser [timeout](/docs/browsers/termination#automatic-deletion-via-timeout)) to persist the Profile. ```typescript Typescript/Javascript theme={null} console.log('Live view:', kernelBrowser.browser_live_view_url); // Navigate and create login state... await kernel.browsers.deleteByID(kernelBrowser.session_id); ``` ```python Python theme={null} print("Live view:", kernel_browser.browser_live_view_url) # Navigate and create login state... await kernel.browsers.delete_by_id(kernel_browser.session_id) ``` ```go Go theme={null} fmt.Println("Live view:", kernelBrowser.BrowserLiveViewURL) // Navigate and create login state... if err := client.Browsers.DeleteByID(ctx, kernelBrowser.SessionID); err != nil { panic(err) } ``` ## 4. Start a new session with the saved profile (read-only) Create another browser using the same profile name. Omitting `save_changes` leaves the stored profile untouched. ```typescript Typescript/Javascript theme={null} const kernelBrowser2 = await kernel.browsers.create({ profile: { name: 'profiles-demo' }, }); console.log('Live view:', kernelBrowser2.browser_live_view_url); ``` ```python Python theme={null} kernel_browser2 = await kernel.browsers.create( profile={"name": "profiles-demo"} ) print("Live view:", kernel_browser2.browser_live_view_url) ``` ```go Go theme={null} kernelBrowser2, err := client.Browsers.New(ctx, kernel.BrowserNewParams{ Profile: shared.BrowserProfileParam{ Name: kernel.String("profiles-demo"), }, }) if err != nil { panic(err) } fmt.Println("Live view:", kernelBrowser2.BrowserLiveViewURL) ``` ## Override opening existing tabs in a new session By default, Profiles restore existing tabs saved in the profile. Pass `start_url` with the profile to clear those restored tabs and open a specific page when the new browser starts. Managed Auth controls the tab state of profiles attached to auth connections. Each login or automatic re-authentication starts in a single tab at the configured login URL, or at the domain homepage when no login URL is configured. After a successful authentication, Kernel saves the resulting tab state with the updated authentication data. Failed and canceled sessions leave the saved profile unchanged. Don't depend on previous tabs or windows remaining in a Managed Auth profile. Set `start_url` when you create a browser if your automation requires a specific first page. ```typescript Typescript/Javascript theme={null} const browser = await kernel.browsers.create({ profile: { name: 'profiles-demo' }, start_url: 'https://example.com/dashboard', }); ``` ```python Python theme={null} browser = await kernel.browsers.create( profile={"name": "profiles-demo"}, start_url="https://example.com/dashboard", ) ``` ```go Go theme={null} browser, err := client.Browsers.New(ctx, kernel.BrowserNewParams{ Profile: shared.BrowserProfileParam{ Name: kernel.String("profiles-demo"), }, StartURL: kernel.String("https://example.com/dashboard"), }) if err != nil { panic(err) } _ = browser ``` The same behavior applies to browser pools configured with both a profile and start url. ## Loading a profile into an existing browser You can load a profile into a browser after it has been created using the [update browser endpoint](https://kernel.sh/docs/api-reference/browsers/update-browser-session). ```typescript Typescript/Javascript theme={null} // Create a browser without a profile const kernelBrowser = await kernel.browsers.create(); // Later, load a profile into the browser await kernel.browsers.update(kernelBrowser.session_id, { profile: { name: 'profiles-demo' } }); ``` ```python Python theme={null} # Create a browser without a profile kernel_browser = await kernel.browsers.create() # Later, load a profile into the browser await kernel.browsers.update(kernel_browser.session_id, profile={"name": "profiles-demo"}) ``` ```go Go theme={null} // Create a browser without a profile kernelBrowser, err := client.Browsers.New(ctx, kernel.BrowserNewParams{}) if err != nil { panic(err) } // Later, load a profile into the browser _, err = client.Browsers.Update(ctx, kernelBrowser.SessionID, kernel.BrowserUpdateParams{ Profile: shared.BrowserProfileParam{ Name: kernel.String("profiles-demo"), }, }) if err != nil { panic(err) } ``` You cannot load a profile into a browser that was already created with a profile. The browser must have been created without any profile configuration. To use profiles with browser pools, see [Profiles with browser pools](/docs/browsers/pools#profiles-with-browser-pools) ## Other ways to use profiles The API and SDKs support listing, deleting, and downloading profile data as JSON. See the [API reference](https://kernel.sh/docs/api-reference/profiles/list-profiles) for more details. ## Multiple auth connections per profile A profile can have any number of auth connections, each for a different domain. When you launch a browser with that profile, it loads the saved authentication state for every connected domain. ### Multi-site workflows If your agent interacts with multiple sites in one workflow, attach an auth connection for each site to one profile. The browser loads the saved authentication state for all of them: ```typescript TypeScript theme={null} // Create a single profile with auth connections for three sites const gmailAuth = await kernel.auth.connections.create({ domain: 'gmail.com', profile_name: 'workflow-bot', }); const slackAuth = await kernel.auth.connections.create({ domain: 'slack.com', profile_name: 'workflow-bot', }); const crmAuth = await kernel.auth.connections.create({ domain: 'crm.example.com', profile_name: 'workflow-bot', }); // Authenticate each connection (omitted for brevity) // Launch a browser with the saved state for all three sites const browser = await kernel.browsers.create({ profile: { name: 'workflow-bot' }, stealth: true, }); ``` ```python Python theme={null} # Create a single profile with auth connections for three sites gmail_auth = await kernel.auth.connections.create( domain="gmail.com", profile_name="workflow-bot", ) slack_auth = await kernel.auth.connections.create( domain="slack.com", profile_name="workflow-bot", ) crm_auth = await kernel.auth.connections.create( domain="crm.example.com", profile_name="workflow-bot", ) # Authenticate each connection (omitted for brevity) # Launch a browser with the saved state for all three sites browser = await kernel.browsers.create( profile={"name": "workflow-bot"}, stealth=True, ) ``` ```go Go theme={null} // Create a single profile with auth connections for three sites gmailAuth, err := client.Auth.Connections.New(ctx, kernel.AuthConnectionNewParams{ ManagedAuthCreateRequest: kernel.ManagedAuthCreateRequestParam{ Domain: "gmail.com", ProfileName: "workflow-bot", }, }) if err != nil { panic(err) } _ = gmailAuth slackAuth, err := client.Auth.Connections.New(ctx, kernel.AuthConnectionNewParams{ ManagedAuthCreateRequest: kernel.ManagedAuthCreateRequestParam{ Domain: "slack.com", ProfileName: "workflow-bot", }, }) if err != nil { panic(err) } _ = slackAuth crmAuth, err := client.Auth.Connections.New(ctx, kernel.AuthConnectionNewParams{ ManagedAuthCreateRequest: kernel.ManagedAuthCreateRequestParam{ Domain: "crm.example.com", ProfileName: "workflow-bot", }, }) if err != nil { panic(err) } _ = crmAuth // Authenticate each connection (omitted for brevity) // Launch a browser with the saved state for all three sites browser, err := client.Browsers.New(ctx, kernel.BrowserNewParams{ Profile: shared.BrowserProfileParam{ Name: kernel.String("workflow-bot"), }, Stealth: kernel.Bool(true), }) if err != nil { panic(err) } _ = browser ``` ### User-to-profile mapping If your platform has end users who each need their own set of authenticated accounts, map each user to a single profile. Attach all of that user's accounts as auth connections on their profile: ```typescript TypeScript theme={null} // For each user on your platform, create one profile // and attach all their accounts as auth connections const userId = 'user-123'; await kernel.auth.connections.create({ domain: 'gmail.com', profile_name: userId, }); await kernel.auth.connections.create({ domain: 'linkedin.com', profile_name: userId, }); await kernel.auth.connections.create({ domain: 'github.com', profile_name: userId, }); // When user-123 triggers a workflow, launch a browser with their profile const browser = await kernel.browsers.create({ profile: { name: userId }, stealth: true, }); ``` ```python Python theme={null} # For each user on your platform, create one profile # and attach all their accounts as auth connections user_id = "user-123" await kernel.auth.connections.create( domain="gmail.com", profile_name=user_id, ) await kernel.auth.connections.create( domain="linkedin.com", profile_name=user_id, ) await kernel.auth.connections.create( domain="github.com", profile_name=user_id, ) # When user-123 triggers a workflow, launch a browser with their profile browser = await kernel.browsers.create( profile={"name": user_id}, stealth=True, ) ``` ```go Go theme={null} // For each user on your platform, create one profile // and attach all their accounts as auth connections userID := "user-123" if _, err := client.Auth.Connections.New(ctx, kernel.AuthConnectionNewParams{ ManagedAuthCreateRequest: kernel.ManagedAuthCreateRequestParam{ Domain: "gmail.com", ProfileName: userID, }, }); err != nil { panic(err) } if _, err := client.Auth.Connections.New(ctx, kernel.AuthConnectionNewParams{ ManagedAuthCreateRequest: kernel.ManagedAuthCreateRequestParam{ Domain: "linkedin.com", ProfileName: userID, }, }); err != nil { panic(err) } if _, err := client.Auth.Connections.New(ctx, kernel.AuthConnectionNewParams{ ManagedAuthCreateRequest: kernel.ManagedAuthCreateRequestParam{ Domain: "github.com", ProfileName: userID, }, }); err != nil { panic(err) } // When user-123 triggers a workflow, launch a browser with their profile browser, err := client.Browsers.New(ctx, kernel.BrowserNewParams{ Profile: shared.BrowserProfileParam{ Name: kernel.String(userID), }, Stealth: kernel.Bool(true), }) if err != nil { panic(err) } _ = browser ``` ## Notes * A profile's `name` must be unique within your [project](/docs/info/projects). The same name can be reused across different projects in the same org. * Profiles store cookies and local storage. Start the session with `save_changes: true` to write changes back when the browser is closed. * To keep a profile immutable for a run, omit `save_changes` (default) when creating the browser. * Multiple browsers in parallel can use the same profile, but only one browser can safely write (`save_changes: true`) to it at a time. Each save overwrites the whole profile, so the browser that ends last wins. * `save_changes` applies to a profile attached to a single browser — either at creation (`kernel.browsers.create()`) or loaded afterward with `kernel.browsers.update()`. A profile set on a [browser pool's](/docs/browsers/pools) config is loaded read-only and never persisted; `save_changes` sent on a pool's profile is silently ignored. To persist per-user state through a pool, attach the profile with `save_changes: true` after acquiring the browser and release with `reuse: false` — see [Per-user profiles with pools](/docs/browsers/pools#per-user-profiles-with-browser-pools). * Profile data is encrypted end to end using a per-organization key. # Programmatic Flow Source: https://kernel.sh/docs/auth/programmatic Build your own credential collection UI with full control build your own credential collection ui instead of using the hosted page. stream login events, render the canonical fields and choices, then submit the user's response with the current interaction id. use the programmatic flow when: * you need a custom credential collection ui * you're building headless authentication * you already store credentials and want to handle only the inputs KERNEL cannot resolve automatically ## How it works Create a managed auth connection, then call `.login()`. Follow the connection's sse stream. When `flow_step` becomes `AWAITING_INPUT`, render `fields` and `choices` from the event. Send the event's `interaction_id` with either `field_values` or `selected_choice_id`. Keep listening because the next page may produce another interaction. ## Interaction contract Every paused interaction uses these properties together: | Property | Purpose | | ---------------- | ------------------------------------------------------------------------------------------ | | `interaction_id` | Opaque id for the current pause. It changes when the actionable screen changes. | | `fields` | Values the user must provide. | | `choices` | Visible routes the user may select, including mfa, sso, account, and organization choices. | Submit the current `interaction_id` with either `field_values` or `selected_choice_id`. Do not mix properties from different events. KERNEL rejects stale interaction ids so a delayed submission cannot act on a newer screen. ## Get started ### 1. Create a connection A managed auth connection attaches one authenticated domain to a [profile](/docs/auth/profiles). A profile can hold multiple connections. ```typescript TypeScript theme={null} const auth = await kernel.auth.connections.create({ domain: 'github.com', profile_name: 'github-profile', }); ``` ```python Python theme={null} auth = await kernel.auth.connections.create( domain="github.com", profile_name="github-profile", ) ``` ```go Go theme={null} auth, err := client.Auth.Connections.New(ctx, kernel.AuthConnectionNewParams{ ManagedAuthCreateRequest: kernel.ManagedAuthCreateRequestParam{ Domain: "github.com", ProfileName: "github-profile", }, }) if err != nil { panic(err) } ``` ### 2. Start a login session ```typescript TypeScript theme={null} await kernel.auth.connections.login(auth.id); ``` ```python Python theme={null} await kernel.auth.connections.login(auth.id) ``` ```go Go theme={null} _, err = client.Auth.Connections.Login(ctx, auth.ID, kernel.AuthConnectionLoginParams{}) if err != nil { panic(err) } ``` A successful interactive login can save submitted credentials for automatic reauthentication. During TOTP flows, Kernel attempts to provide the authenticator code automatically. ### 3. Stream and submit Listen for `AWAITING_INPUT`. Submit fields or a selected choice, then keep listening for the next interaction. ```typescript TypeScript theme={null} const events = await kernel.auth.connections.follow(auth.id); for await (const event of events) { if ( event.event !== 'managed_auth_state' || event.flow_step !== 'AWAITING_INPUT' || !event.interaction_id ) { continue; } if (event.fields?.length) { const fieldValues: Record = {}; for (const field of event.fields) { fieldValues[field.id] = await promptUser(field); } await kernel.auth.connections.submit(auth.id, { interaction_id: event.interaction_id, field_values: fieldValues, }); } else if (event.choices?.length) { const choice = await promptUserToChoose(event.choices); await kernel.auth.connections.submit(auth.id, { interaction_id: event.interaction_id, selected_choice_id: choice.id, }); } } ``` `promptUser` and `promptUserToChoose` represent your application's ui. The submission examples below show the same requests in each sdk. every programmatic login session also has a `hosted_url`. redirect the user there if you want the hosted ui to finish an unexpected state. In the examples below, `state` is the current `managed_auth_state` event. ## Fields Each field includes: | Property | Meaning | | -------- | ------------------------------------------------------------------------ | | `id` | Stable id used as the key in `field_values`. | | `ref` | Credential meaning, such as `email`, `password`, or `sms_code`. | | `type` | `identifier`, `password`, `code`, `totp_code`, `totp_secret`, or `text`. | | `label` | Text to show beside the input. | | `reason` | `missing` or `rejected`. | | `hint` | Optional context, including a masked code destination. | Submit values by **field id**, not by `ref`: ```typescript TypeScript theme={null} await kernel.auth.connections.submit(auth.id, { interaction_id: state.interaction_id, field_values: { [state.fields[0].id]: userValue, }, }); ``` ```python Python theme={null} await kernel.auth.connections.submit( auth.id, interaction_id=state.interaction_id, field_values={state.fields[0].id: user_value}, ) ``` ```go Go theme={null} _, err := client.Auth.Connections.Submit(ctx, auth.ID, kernel.AuthConnectionSubmitParams{ SubmitFieldsRequest: kernel.SubmitFieldsRequestParam{ InteractionID: kernel.String(state.InteractionID), FieldValues: map[string]string{ state.Fields[0].ID: userValue, }, }, }) if err != nil { panic(err) } ``` ### Replacing a rejected credential A field with `reason: 'rejected'` means the site explicitly refused the previous value. Prompt for a new value and submit it against the new interaction: ```typescript TypeScript theme={null} const rejected = state.fields?.filter(field => field.reason === 'rejected'); if (state.interaction_id && rejected?.length) { const corrected = await promptUser(rejected); const fieldValues = Object.fromEntries( rejected.map(field => [field.id, corrected[field.id]]) ); await kernel.auth.connections.submit(auth.id, { interaction_id: state.interaction_id, field_values: fieldValues, }); } ``` ```python Python theme={null} rejected = [field for field in (state.fields or []) if field.reason == "rejected"] if state.interaction_id and rejected: corrected = await prompt_user(rejected) field_values = { field.id: corrected[field.id] for field in rejected } await kernel.auth.connections.submit( auth.id, interaction_id=state.interaction_id, field_values=field_values, ) ``` An unattended reauth run does not ask for a corrected credential. It reports the exact terminal rejection—such as `credentials_invalid` or `totp_code_rejected`—rather than repeating a value the site already rejected. ## Choices All selectable auth routes use the same shape. `choice.type` identifies the category: * `mfa_method` * `sso_provider` * `sign_in_method` * `auth_method` * `identifier_method` * `account` * `other` Render the visible `label`, optional `description`, and optional `masked_destination`. Submit the exact `choice.id` returned by the event: ```typescript TypeScript theme={null} const mfaChoices = state.choices?.filter(choice => choice.type === 'mfa_method') ?? []; const selected = await choose(mfaChoices); await kernel.auth.connections.submit(auth.id, { interaction_id: state.interaction_id, selected_choice_id: selected.id, }); ``` ```python Python theme={null} mfa_choices = [choice for choice in (state.choices or []) if choice.type == "mfa_method"] selected = await choose(mfa_choices) await kernel.auth.connections.submit( auth.id, interaction_id=state.interaction_id, selected_choice_id=selected.id, ) ``` ```go Go theme={null} _, err := client.Auth.Connections.Submit(ctx, auth.ID, kernel.AuthConnectionSubmitParams{ SubmitFieldsRequest: kernel.SubmitFieldsRequestParam{ InteractionID: kernel.String(state.InteractionID), SelectedChoiceID: kernel.String(selected.ID), }, }) if err != nil { panic(err) } ``` Do not derive the submitted id from the label or mfa type. Two sms choices can have different masked destinations and different grounded targets. ### Account and organization pickers Account and organization rows are choices with `type: 'account'` or another non-mfa choice type. Show every returned row and submit the selected stable id: ```typescript theme={null} const accounts = state.choices?.filter(choice => choice.type === 'account'); for (const account of accounts ?? []) { console.log(account.label, account.description); } ``` Stored credential values are not returned for matching. Use only the masked or display context present on each choice. ## External actions When `flow_step` is `AWAITING_EXTERNAL_ACTION`, show `external_action_message` and keep listening. The flow resumes when the external action completes. Some external-action screens also expose fallback `fields` or `choices`. If `interaction_id` is present, submit a fallback through the same canonical contract: ```typescript theme={null} if ( state.flow_step === 'AWAITING_EXTERNAL_ACTION' && state.interaction_id && state.choices?.length ) { const fallback = await choose(state.choices); await kernel.auth.connections.submit(auth.id, { interaction_id: state.interaction_id, selected_choice_id: fallback.id, }); } ``` ## Step reference | Step | Description | | -------------------------- | ------------------------------------------------------------------------------ | | `DISCOVERING` | Finding and inspecting the login surface. | | `AWAITING_INPUT` | Waiting for canonical fields or choices. | | `AWAITING_EXTERNAL_ACTION` | Waiting for an out-of-browser action; canonical fallbacks may also be present. | | `SUBMITTING` | Processing the submitted interaction. | | `COMPLETED` | The flow has finished. | ## Status reference | Status | Description | | ------------- | ------------------------------------------------------- | | `IN_PROGRESS` | Authentication is ongoing. | | `SUCCESS` | Login completed and the profile was saved. | | `FAILED` | Login failed; inspect `error_code` and `error_message`. | | `EXPIRED` | The flow timed out. | | `CANCELED` | The flow was canceled or superseded. | The connection's overall `status` is `AUTHENTICATED` or `NEEDS_AUTH`. ## Connection configuration Connection-level options such as a custom login url, allowed domains, proxy, session recording, and health-check interval apply to both hosted and programmatic flows. See [connection configuration](/docs/auth/configuration). ## SSE stream behavior `auth.connections.follow()` opens: ``` GET /auth/connections/{id}/events ``` The stream delivers `managed_auth_state` events and closes when the flow succeeds, fails, expires, or is canceled. Prefer the stream over polling so your ui receives each interaction id in order. # React Component Source: https://kernel.sh/docs/auth/react Embed the Kernel managed auth login flow in your own app with a drop-in React component [`@onkernel/managed-auth-react`](https://www.npmjs.com/package/@onkernel/managed-auth-react) is a React component library that renders the entire managed auth login flow inside your own app. It calls the same APIs as the [Hosted UI](/docs/auth/hosted-ui), but renders on your origin so it inherits your styling, domain, and CSP. Use the React component when: * You want the [Hosted UI](/docs/auth/hosted-ui) experience but rendered inside your own app * You want to fully restyle the login UI to match your brand without rebuilding the flow * You want same-origin auth traffic for cookies, CSP, or observability ## Install ```bash theme={null} bun add @onkernel/managed-auth-react # or: npm install @onkernel/managed-auth-react ``` ## Getting started ### 1. Start a Login Session on your backend Same as the Hosted UI flow — create a connection and start a login. The login response returns the connection `id` and a one-time `handoff_code`; those are the two values you'll hand to the component on the frontend. ```typescript TypeScript theme={null} const auth = await kernel.auth.connections.create({ domain: 'netflix.com', profile_name: 'user-123', }); const { id, handoff_code } = await kernel.auth.connections.login(auth.id); ``` ```python Python theme={null} auth = await kernel.auth.connections.create( domain="netflix.com", profile_name="user-123", ) login = await kernel.auth.connections.login(auth.id) # login.id, login.handoff_code ``` ```go Go theme={null} auth, err := client.Auth.Connections.New(ctx, kernel.AuthConnectionNewParams{ ManagedAuthCreateRequest: kernel.ManagedAuthCreateRequestParam{ Domain: "netflix.com", ProfileName: "user-123", }, }) if err != nil { panic(err) } login, err := client.Auth.Connections.Login(ctx, auth.ID, kernel.AuthConnectionLoginParams{}) if err != nil { panic(err) } fmt.Println(login.ID, login.HandoffCode) ``` The login response also includes a `hosted_url`. That URL points at Kernel's own hosted login page and is only relevant if you're using the [Hosted UI](/docs/auth/hosted-ui) flow. When you're embedding the React component in your own app, ignore `hosted_url` and just hand `id` + `handoff_code` to your frontend however your app normally routes — a redirect with them as path/query params, a popup, props on the same page, etc. ### 2. Render `` on the frontend Pass the `id` and `handoff_code` from your backend into the component. How you get them to the page is up to you — the example below shows a Next.js App Router route that surfaces `id` as a path param and `code` as a query param, but any plumbing works. ```tsx app/login/[id]/page.tsx theme={null} "use client"; import { use } from "react"; import { useSearchParams } from "next/navigation"; import { KernelManagedAuth } from "@onkernel/managed-auth-react"; import "@onkernel/managed-auth-react/styles.css"; export default function LoginPage({ params, }: { params: Promise<{ id: string }>; }) { const { id } = use(params); const code = useSearchParams().get("code") ?? ""; return ( { window.location.href = `/connected?profile=${profileName}`; }} onError={({ code, message }) => { console.error(code, message); }} /> ); } ``` The component is client-only — `"use client"` is required in any RSC framework (Next.js App Router, Remix, etc.). ## Backend connectivity By default the component talks directly to `https://api.onkernel.com`. That works out of the box; nothing else to configure. If you'd rather keep all auth traffic same-origin (cookies, CSP, observability), set `baseUrl=""` and proxy the three endpoints the package hits through your own framework: ```ts next.config.ts theme={null} export default { async rewrites() { return [ { source: "/auth/connections/:id/exchange", destination: `${process.env.KERNEL_BASE_URL}/auth/connections/:id/exchange`, }, { source: "/auth/connections/:id", destination: `${process.env.KERNEL_BASE_URL}/auth/connections/:id`, }, { source: "/auth/connections/:id/submit", destination: `${process.env.KERNEL_BASE_URL}/auth/connections/:id/submit`, }, ]; }, }; ``` ```tsx theme={null} ``` ## Styling Pass an `appearance` prop to restyle any part of the component. Four composable layers: ### Design tokens ```tsx theme={null} ``` Every variable becomes a `--kma-*` CSS custom property on the component root, so you can also wire them up from your own stylesheet. ### Per-element overrides Every rendered element has a stable key. Target it with a class, a style object, or both: ```tsx theme={null} ``` Style objects support nested pseudo-state selectors (`:hover`, `:focus`, `:focus-visible`, `:active`, `:disabled`, `::placeholder`) which compile to scoped CSS at runtime. Every rendered element also carries a `data-kma-element=""` attribute, so you can target them from global CSS: ```css theme={null} [data-kma-element="buttonPrimary"] { text-transform: uppercase; } ``` ### Layout toggles ```tsx theme={null} ``` ### Theme ```tsx theme={null} // "light" | "dark" | "auto" (default: auto, respects prefers-color-scheme) ``` ## Localization Pass a partial map of string overrides; every key not provided falls back to English. ```tsx theme={null} `Connectez-vous à ${site}`, primeContinueButton: "Continuer", submitButton: "Se connecter", mfaTypeLabels: { sms: "SMS", email: "E-mail", switch: "Autre méthode" }, }} {...rest} /> ``` ## Props | Prop | Type | Required | Default | Description | | -------------- | --------------------------------- | -------- | ---------------------------- | ---------------------------------------------------------------------------------------- | | `sessionId` | `string` | yes | — | Managed auth connection `id` from the `.login()` response. | | `handoffCode` | `string` | yes | — | Single-use `handoff_code` from the `.login()` response, exchanged for a JWT. | | `appearance` | `Appearance` | no | — | Styling — variables, elements, layout, theme. | | `localization` | `Localization` | no | English | Partial string overrides. | | `onSuccess` | `(p: AuthSuccessPayload) => void` | no | — | Fires on `SUCCESS`. Payload: `{ profileName: string; domain: string }`. | | `onError` | `(p: AuthErrorPayload) => void` | no | — | Fires on `FAILED`, `CANCELED`, `EXPIRED`. Payload: `{ code?: string; message: string }`. | | `baseUrl` | `string` | no | `"https://api.onkernel.com"` | Override the Kernel API origin. Use `""` for same-origin proxying via your own rewrites. | | `fetch` | `typeof fetch` | no | `globalThis.fetch` | Inject a custom fetch (for SSR or instrumentation). | ## Headless step components For most integrations `` is all you need. If you want to drive the UI yourself — custom controllers, test harnesses, or a non-standard flow — the underlying step components are exported individually: ```tsx theme={null} import { Shell, StepPrime, StepSuccess, StepError, StepExpired, LoadingState, ExternalActionWaiting, UnifiedAuthForm, AppearanceProvider, LocalizationProvider, } from "@onkernel/managed-auth-react"; ``` Wrap them in `` and `` to inherit the same styling/localization plumbing as the all-in-one component. ## Connection Configuration Connection-level options — custom login URL, SSO/OAuth, custom proxy, session recording, post-login URL, and updates — are set on `auth.connections.create` (or later via `auth.connections.update`) and apply equally regardless of which integration flow you use. See [Connection Configuration](/docs/auth/configuration). ## Reference integration A full reference integration lives in the [`kernel/managed-auth-react`](https://github.com/kernel/managed-auth-react) repo's demo app. It's a copy-pasteable starting point for embedding the component in your own app. # Bots and agents Source: https://kernel.sh/docs/bots Kernel's bots and agents, their purposes, and how to verify them with Web Bot Auth Kernel identifies its bots and agents with [Web Bot Auth](https://datatracker.ietf.org/doc/html/draft-meunier-web-bot-auth-architecture). Each identity serves its own key directory from its own authority, so site owners can allow or block each one **independently by purpose** — for example, allow search indexing while blocking user-directed agents (or vice versa). Kernel is listed in [Cloudflare's bots and agents directory](https://radar.cloudflare.com/bots/directory/kernel) and [Vercel's public directory](https://bots.fyi/?query=kernel). ## Kernel Agent User-directed browser automation. Kernel Agent visits pages on behalf of an end user's request; it is not an automatic crawler. Requests are signed with Web Bot Auth rather than a dedicated crawler user-agent token. | Field | Value | | --------------- | --------------------------------------------------------------------- | | Purpose | Agent | | Operator | Intermediary (end-user directed) | | Signature-Agent | `https://www.kernel.sh` | | Key directory | `https://www.kernel.sh/.well-known/http-message-signatures-directory` | ## Kernel Search Crawls pages to build search indexes and retrieval databases. Kernel Search identifies itself with the `KernelSearchBot` user-agent token and follows `robots.txt` directives for that token, including crawl-delay preferences. | Field | Value | | --------------- | ---------------------------------------------------------------------------- | | Purpose | Search | | Operator | Direct (Kernel-operated) | | User-Agent | `KernelSearchBot` | | Signature-Agent | `https://search.bot.kernel.sh` | | Key directory | `https://search.bot.kernel.sh/.well-known/http-message-signatures-directory` | ## Verifying Kernel traffic Each identity publishes its public key set (JWKS) at its key directory. To verify a request: 1. Read the `Signature-Agent` header to determine which Kernel identity signed the request. 2. Fetch the public key set from that identity's key directory and cache it per the `Cache-Control` header. 3. Verify the `Signature` and `Signature-Input` headers per [RFC 9421](https://datatracker.ietf.org/doc/html/rfc9421). Most major bot-detection services, CDNs, and WAFs verify Web Bot Auth automatically. See [Web Bot Auth](/docs/browsers/bot-detection/web-bot-auth) for how Kernel signs requests. ## Contact For questions about Kernel bot or agent traffic, contact [support@kernel.sh](mailto:support@kernel.sh). # hCaptcha Source: https://kernel.sh/docs/browsers/bot-detection/hcaptcha Kernel's hCaptcha solver is a beta feature for teams that need help handling hCaptcha challenges in browser automations. When enabled for your organization, Kernel can attempt to solve supported hCaptcha challenges automatically from Kernel browsers. This is useful for permitted automation where hCaptcha appears as part of a normal browser workflow, such as QA, account operations, or user-authorized agent tasks. The hCaptcha solver is in beta and isn't enabled for all organizations by default. ## Get access To use the hCaptcha solver, [contact Kernel support](https://www.kernel.sh/docs/info/support) and ask to have the hCaptcha beta enabled for your organization. Include the website or workflow you're testing, your expected volume, and whether you're already using [stealth mode](/docs/browsers/bot-detection/stealth), [profiles](/docs/auth/profiles), or custom [proxies](/docs/proxies/overview). This helps us confirm the right setup for your use case. # Overview Source: https://kernel.sh/docs/browsers/bot-detection/overview Automating the web as a browser agent is hard. Modern websites deploy increasingly sophisticated bot detection systems to prevent fraud, abuse, and denial-of-service attacks. Even if you have explicit permission to automate — for example, QA'ing your own site or performing actions on behalf of users with their consent — these systems often assume any automated browser is hostile. Kernel provides tools to help with these challenges. Under the hood, our browsers are optimized for realistic environments. **Everything we do is open source and inspectable — you can view our build configurations and runtime layers [here](https://github.com/onkernel/kernel-images).** This guide explains how bot detection works at a high level, common pitfalls to avoid, and how Kernel's features can help your automations run reliably. ## How Bot Detection Works Most detection systems look for inconsistencies between how a real user's browser behaves and how an automated one does. Common giveaways include: * **IP addresses**: IPs from data centers (AWS, GCP, Azure) * **Browser environment**: unusual viewport sizes, incorrect timezones, or missing APIs * **Automation frameworks**: traces of Playwright, Puppeteer, or Chrome DevTools Protocol (CDP) connections * **Headless browsers** — browsers started without a visible window expose subtle differences (rendering, GPU, fonts) * **Typing/clicking signals** — identical cursor paths, uniform typing speeds, or rapid mouse movements * **Metadata** — mismatched cookies, inconsistent user-agent strings These systems are heuristic and probabilistic — small mismatches can still trigger blocks. The goal isn't to “beat” detection but rather emulate the real-world conditions of a normal browser session. ## Kernel Features That Help ### Anti-detection defaults Every Kernel browser launches with anti-detection chrome configuration applied. No setup required. ### [Stealth Mode](/docs/browsers/bot-detection/stealth) On top of the defaults, stealth mode adds a default ISP proxy and an automatic CAPTCHA solver. Both are opt-out so you can BYO proxy and/or CAPTCHA tooling. ### [Configurable Proxies](/docs/proxies/overview) Bring your own proxy network or use Kernel's managed proxy pool (selectable down to ZIP-code level). If needed, use the same IP to reduce detection and allow for regional testing or QA. ### [Profiles](/docs/auth/profiles) Profiles persist cookies, local storage, and session data between runs. Combined with a fixed proxy, this mimics a returning user. We recommend using them to persist authenticated states and reduce CAPTCHAs. ### [Browser Pools](/docs/browsers/pools) Browser pools let you reuse browsers across multiple visits to the same website, which introduces consistency with respect to the IP address. Since IP addresses are one of the main components of fingerprinting used by modern bot detection systems, browser pools drastically increase your chances of avoiding detection. ### [Playwright Execution API](/docs/browsers/playwright-execution) Executes Playwright scripts in the same VM as the browser, ensuring headers, user-agent strings, and environment match. Kernel automatically applies Patchright to remove automation fingerprints, including headless indicators. ### [Computer Controls API](/docs/browsers/computer-controls) Controls the browser without using the Chrome DevTools Protocol (CDP), which can reduce bot detection signals. Emulates native keyboard and mouse input directly at the OS level and includes human-like [bezier curves](/docs/browsers/computer-controls#move-the-mouse) by default. ### [GPU Acceleration](/docs/browsers/gpu-acceleration) Many detection systems fingerprint canvas and WebGL rendering output and cross-check it against the claimed GPU. Software-rendered browsers produce pixel hashes that don't match any real consumer GPU, which is a strong bot signal on sites with rendering-based fingerprinting. GPU-enabled Kernel browsers render through real hardware, producing output consistent with a normal user's device. ## Getting Started Before you start automating your workflow, we recommend that you manually test your website to understand how it behaves with Kernel's browsers. Here's how to do that: 1. **Launch a browser from the [Kernel dashboard](https://dashboard.onkernel.com/browsers).** This opens a Kernel browser instance in a clean virtual machine. 2. **Navigate to the target website** and perform the same actions you plan to automate — logging in, filling forms, loading dashboards, etc. 3. **Observe potential friction points:** * Are you immediately prompted for CAPTCHA or MFA? * Does the site behave differently across geographies? * Are there rate limits, redirects, or blocked resources? 4. **Adjust environment settings** — such as proxy configurations — until the manual session works smoothly. Once you have a stable baseline, replicate those conditions in your automations. ## Recommended Practices | Category | Recommendation | | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Viewport & Display** | Use Kernel's default viewport — we've tuned it to mirror realistic device profiles. | | **Headless Mode** | Avoid headless mode. Kernel runs full, rendered browsers by default. | | **User Agent Headers** | Don't override headers manually. Let Kernel manage them for minimize mismatches. | | **Execution Method** | Prefer our **Playwright Execution API** or **Computer Controls API** over self-hosted Playwright/Puppeteer. | | **Navigation Path** | Avoid navigating directly to a deep URL or login page. Start at the site's home page and click through to where you want to go — a real user rarely lands on a deep link with no referrer or prior page views. | | **Session Persistence** | Use **Profiles** to retain cookies and local storage between sessions. | | **Typing & Scrolling** | Add natural variation to interaction timing. | | **Rate Limits** | Many sites monitor request frequency; rapid / concurrent actions can trigger blocking. | | **Network Identity** | Use stable IP addresses, especially if logging in. See [Choosing a proxy type](#choosing-a-proxy-type) below. | | **Extensions** | Use the [Extensions API](/docs/browsers/extensions) carefully — each adds its own fingerprint, which can be detected. | ## Choosing a Proxy Type IP address is one of the strongest signals bot detection systems use. Kernel offers several [proxy types](/docs/proxies/overview), each with different trade-offs for detection avoidance. ### ISP proxies [ISP proxies](/docs/proxies/isp) route traffic through data centers using IP addresses assigned by real internet service providers. They offer datacenter-level speed with better legitimacy than pure datacenter proxies, and every connection in a session exits through the same static IP — making them ideal for login flows and session-based workflows. Kernel's [stealth mode](/docs/browsers/bot-detection/stealth) uses static ISP proxies that are hosted in data centers but announced on residential ISP networks, so they tend to appear residential by ASN to most of the internet. This matters for IP-reputation-based detection systems: a static IP on a residential ASN looks like a normal ISP customer, which generally achieves better pass rates than rotating residential IPs. ### Residential proxies [Residential proxies](/docs/proxies/residential) route traffic through real consumer devices, making them the least detectable proxy type by ASN classification. However, exit IPs [rotate per connection](/docs/proxies/residential#ip-rotation-behavior) since the underlying devices come online and offline dynamically — different tabs hitting different domains will likely show different public IPs. Some IP-reputation-based detection systems (such as reCAPTCHA) can detect rotating proxy-pool traffic patterns and penalize them, regardless of how clean the individual exit IPs are. On the other hand, residential proxies tend to be a stronger choice against fingerprint-heavy vendors where detection focuses on the browser and behavioral layer rather than the network layer. Residential proxies also offer richer geo-targeting (country, state, city, ZIP, ASN) compared to ISP. ### Datacenter proxies [Datacenter proxies](/docs/proxies/datacenter) are the fastest and most cost-effective option, but their IP ranges are well-known to detection systems. Some sites block datacenter IPs outright; others treat them with higher scrutiny. ### Which to use Start with ISP — it's the stealth default for good reason. Consider residential if you need fine-grained geo-targeting or your specific target site doesn't rely on IP reputation as its primary detection signal. Use datacenter when speed and cost matter more than detection avoidance. # Stealth Mode Source: https://kernel.sh/docs/browsers/bot-detection/stealth All Kernel browsers ship with anti-detection optimizations by default — you don't need stealth mode for this baseline. Enabling `stealth` mode adds two managed services on top: 1. **Default proxy** — traffic routes through a static [ISP proxy](/docs/proxies/isp), providing a stable exit IP for the session. 2. **Automatic CAPTCHA solver** — solves [reCAPTCHAs](https://www.google.com/recaptcha/api2/demo), Cloudflare challenges, and similar tests automatically. Both are opt-out so you can [bring your own](#bring-your-own-proxy-or-captcha-solver) where it makes sense. ### IP Rotation Behavior The default stealth proxy provides a **static exit IP** — all connections within the session exit through the same IP address. If you override the default with a [residential proxy](/docs/proxies/residential), exit IPs will rotate per connection. See [Residential IP Rotation Behavior](/docs/proxies/residential#ip-rotation-behavior) for details. To turn on stealth mode, set its flag when instantiating Kernel browsers: ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); const kernelBrowser = await kernel.browsers.create({ stealth: true, }); ``` ```python Python theme={null} from kernel import Kernel kernel = Kernel() kernel_browser = kernel.browsers.create( stealth=True, ) ``` ```go Go theme={null} package main import ( "context" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() kernelBrowser, err := client.Browsers.New(ctx, kernel.BrowserNewParams{ Stealth: kernel.Bool(true), }) if err != nil { panic(err) } _ = kernelBrowser } ``` ## Bring your own proxy or CAPTCHA solver Anti-detection is the platform default, so you can freely mix in your own networking or CAPTCHA tooling. Common patterns: * **BYO proxy, keep CAPTCHA solver** — launch a stealth browser with your own [proxy](/docs/proxies/overview) via `proxy_id`. Replaces the ISP default; CAPTCHA solver stays loaded. * **BYO proxy, no CAPTCHA solver** — launch a non-stealth browser with your own `proxy_id`. Full anti-detection config, no managed proxy or CAPTCHA extension. * **Managed proxy, no CAPTCHA solver** — launch a non-stealth browser with a defined managed proxy via `proxy_id`. Full anti-detection config with a Kernel managed proxy and no CAPTCHA extension enabled. * **Disable the default proxy at runtime** — on a running stealth browser, set [`disable_default_proxy`](/docs/proxies/overview#disable-default-proxy-on-stealth-browsers) to route directly while keeping the CAPTCHA solver. ### Stealth with your own proxy To run a stealth browser through your own proxy instead of the managed ISP default, pass both `stealth: true` and a `proxy_id` when creating the session. The CAPTCHA solver stays loaded; your proxy replaces the ISP default. ```typescript Typescript/Javascript theme={null} const kernelBrowser = await kernel.browsers.create({ stealth: true, proxy_id: myProxy.id, }); ``` ```python Python theme={null} kernel_browser = kernel.browsers.create( stealth=True, proxy_id=my_proxy.id, ) ``` ```go Go theme={null} kernelBrowser, err := client.Browsers.New(ctx, kernel.BrowserNewParams{ Stealth: kernel.Bool(true), ProxyID: kernel.String(myProxy.ID), }) if err != nil { panic(err) } _ = kernelBrowser ``` If you're looking for proxy-level configuration with Kernel browsers, see [Proxies](/docs/proxies/overview). ## CAPTCHA Handling Behavior Below are tips for working with Kernel's Stealth Mode auto-CAPTCHA solver across different challenge types and automation frameworks. ### Anthropic Computer Use Anthropic Computer Use stops when it encounters a CAPTCHA. Use Kernel's auto-CAPTCHA solver by adding this to your prompt: `"If you see a CAPTCHA or similar test, just wait for it to get solved automatically by the browser."` ### Cloudflare Challenge When encountering a Cloudflare challenge, our auto-CAPTCHA solver will attempt to handle it. Once the "Ready" message appears on the screen, continue with your intended browser actions (e.g., entering credentials and submitting a login attempt). After the "Ready" message appears, don't click the Cloudflare CAPTCHA checkbox — this can interfere with the solver. # Web Bot Auth Source: https://kernel.sh/docs/browsers/bot-detection/web-bot-auth Cryptographically sign browser requests with Web Bot Auth [Web Bot Auth](https://datatracker.ietf.org/doc/html/draft-meunier-web-bot-auth-architecture) is quickly becoming the standard way for agents to establish identity. That's why we've partnered with [Vercel](https://bots.fyi/d/kernel) and [Cloudflare](https://radar.cloudflare.com/bots/directory/kernel) to support Web Bot Auth on Kernel. Kernel on Vercel's public directory of known bots used across the web You can now cryptographically sign browser requests, so your agents can prove who they are to services like Vercel. ## How it works Web Bot Auth works via a Chrome extension that intercepts all outgoing HTTP requests and adds cryptographic signature headers: * **`Signature`**: The RFC 9421 signature of the request * **`Signature-Input`**: Metadata about how the signature was created * **`Signature-Agent`**: URL that points to your key directory Platforms like [Vercel](https://bots.fyi/) or other hosting providers can verify these signatures against your public key, confirming that the request came from your authenticated agent. ## Quick start with test key The fastest way to get started is using a test key, which works with this [test verification site](https://http-message-signatures-example.research.cloudflare.com/). ### 1. Build the extension Use the Kernel CLI to build the Web Bot Auth extension: ```bash theme={null} kernel extensions build-web-bot-auth --to ./web-bot-auth-ext --upload my-web-bot-auth ``` The build command requires Node.js and npm to be installed on your system. ### 2. Create a browser with the extension ```bash CLI theme={null} # Create a browser with the web-bot-auth extension kernel browsers create --extension my-web-bot-auth # The command outputs the browser ID and live view URL # Open the live view URL in your browser, then navigate to: # https://http-message-signatures-example.research.cloudflare.com/ ``` ```typescript TypeScript theme={null} import { Kernel } from "@onkernel/sdk"; import { chromium } from "playwright"; const kernel = new Kernel(); // Create browser with web-bot-auth extension const browser = await kernel.browsers.create({ extensions: [{ name: "my-web-bot-auth" }], }); // Connect via Playwright const pw = await chromium.connectOverCDP(browser.browser_url); const context = pw.contexts()[0]; const page = context?.pages()[0] || await context.newPage(); // Navigate to a page - requests will be automatically signed await page.goto("https://http-message-signatures-example.research.cloudflare.com/"); ``` ```python Python theme={null} from kernel import Kernel from playwright.sync_api import sync_playwright kernel = Kernel() # Create browser with web-bot-auth extension browser = kernel.browsers.create(extensions=[{"name": "my-web-bot-auth"}]) # Connect via Playwright with sync_playwright() as p: pw = p.chromium.connect_over_cdp(browser.browser_url) context = pw.contexts[0] page = context.pages[0] if context.pages else context.new_page() # Navigate to a page - requests will be automatically signed page.goto("https://http-message-signatures-example.research.cloudflare.com/") ``` ### 3. Verify it's working Navigate to the [test site](https://http-message-signatures-example.research.cloudflare.com/) to verify your signatures are being accepted: This site validates requests signed with the RFC9421 test key and shows whether the signature was verified successfully. ## Using your own keys For production use, you'll want to use your own signing keys instead of the test key. ### 1. Generate an Ed25519 key pair Create a JWK file with your Ed25519 private key. The key must include both the public (`x`) and private (`d`) components: ```json my-key.jwk theme={null} { "kty": "OKP", "crv": "Ed25519", "x": "YOUR_PUBLIC_KEY_BASE64URL", "d": "YOUR_PRIVATE_KEY_BASE64URL" } ``` See [web-bot-auth documentation](https://github.com/cloudflare/web-bot-auth) for tools to generate Ed25519 key pairs. ### 2. Host your public key For websites to verify your signatures, you need to host your public key at a well-known URL. Create a key directory at: ``` https://yourdomain.com/.well-known/http-message-signatures-directory ``` The directory should contain your public keys in JWKS format: ```json theme={null} { "keys": [ { "kty": "OKP", "crv": "Ed25519", "x": "YOUR_PUBLIC_KEY_BASE64URL", "kid": "YOUR_KEY_ID" } ], "purpose": "your-bot-purpose" } ``` ### 3. Build with your key and hosted key directory ```bash theme={null} kernel extensions build-web-bot-auth \ --to ./web-bot-auth-ext \ --key ./my-key.jwk \ --signature-agent https://yourdomain.com \ --upload my-web-bot-auth ``` ### 4. Using Kernel's bot identities Kernel's own Web Bot Auth identities are already approved by Cloudflare, Vercel, Akamai, and other bot-verification providers. If you want to sign requests with one of Kernel's identities rather than your own, [contact Kernel support](https://www.kernel.sh/docs/info/support). See [Bots and agents](/docs/bots) for the list of identities and their key directories. ### 5. Register with Vercel and other Web Bot Auth-aware directories (optional) If you want Vercel-protected sites to recognize your agent, you can register your key directory with [Vercel](https://bots.fyi/new-bot). Kernel is officially listed in the Vercel directory. ## References * [Vercel's Public Directory](https://bots.fyi/?query=kernel) * [Web Bot Auth GitHub Repository](https://github.com/cloudflare/web-bot-auth) * [Web Bot Auth Documentation](https://developers.cloudflare.com/bots/reference/bot-verification/web-bot-auth/) * [RFC 9421 - HTTP Message Signatures](https://datatracker.ietf.org/doc/html/rfc9421) * [Test Verification Site](https://http-message-signatures-example.research.cloudflare.com/) * [Web Bot Auth Architecture Draft](https://thibmeu.github.io/http-message-signatures-directory/draft-meunier-web-bot-auth-architecture.html) # Custom Chrome Policies Source: https://kernel.sh/docs/browsers/chrome-policies Customize Chrome behavior on individual browsers and browser pools using Chrome policies `chrome_policy` is an optional object that applies [Chrome enterprise policies](https://chromeenterprise.google/policies/) to a browser. It works on both individual on-demand browsers (`browsers.create()`) and [browser pools](https://kernel.sh/docs/api-reference/browser-pools/create-a-browser-pool#body-chrome-policy), where it applies to every browser in the pool. Use it to control startup behavior, default homepages, bookmarks, and other browser-level settings. Keys are Chrome policy names and values are the corresponding settings. The same policies and value types apply on both paths. Kernel-managed policies (extensions, proxy, and CDP/automation) are rejected, and the serialized payload is capped at 5 MiB. ## Setting chrome policies on a pool Pass a `chrome_policy` object when [creating](/docs/browsers/pools#create-a-browser-pool) or [updating](/docs/browsers/pools#update-a-browser-pool) a pool. ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); const pool = await kernel.browserPools.create({ name: "my-configured-pool", size: 5, chrome_policy: { HomepageLocation: "https://kernel.sh", HomepageIsNewTabPage: false, ShowHomeButton: true, NewTabPageLocation: "https://kernel.sh/docs", RestoreOnStartup: 4, RestoreOnStartupURLs: ["https://kernel.sh"], BookmarkBarEnabled: true, ManagedBookmarks: [ { toplevel_name: "Company Resources" }, { name: "Dashboard", url: "https://example.com/dashboard" }, { name: "Documentation", url: "https://example.com/docs" }, { name: "Tools", children: [ { name: "Jira Board", url: "https://example.com/jira" }, { name: "Slack", url: "https://example.com/slack" }, { name: "GitHub PRs", url: "https://example.com/github" }, { name: "Runbooks", url: "https://example.com/runbooks" } ] } ] } }); ``` ```python Python theme={null} from kernel import Kernel kernel = Kernel() pool = kernel.browser_pools.create( name="my-configured-pool", size=5, chrome_policy={ "HomepageLocation": "https://kernel.sh", "HomepageIsNewTabPage": False, "ShowHomeButton": True, "NewTabPageLocation": "https://kernel.sh/docs", "RestoreOnStartup": 4, "RestoreOnStartupURLs": ["https://kernel.sh"], "BookmarkBarEnabled": True, "ManagedBookmarks": [ {"toplevel_name": "Company Resources"}, {"name": "Dashboard", "url": "https://example.com/dashboard"}, {"name": "Documentation", "url": "https://example.com/docs"}, { "name": "Tools", "children": [ {"name": "Jira Board", "url": "https://example.com/jira"}, {"name": "Slack", "url": "https://example.com/slack"}, {"name": "GitHub PRs", "url": "https://example.com/github"}, {"name": "Runbooks", "url": "https://example.com/runbooks"} ] } ] } ) ``` ```go Go theme={null} package main import ( "context" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() pool, err := client.BrowserPools.New(ctx, kernel.BrowserPoolNewParams{ Name: kernel.String("my-configured-pool"), Size: 5, ChromePolicy: map[string]any{ "HomepageLocation": "https://kernel.sh", "HomepageIsNewTabPage": false, "ShowHomeButton": true, "NewTabPageLocation": "https://kernel.sh/docs", "RestoreOnStartup": 4, "RestoreOnStartupURLs": []string{"https://kernel.sh"}, "BookmarkBarEnabled": true, "ManagedBookmarks": []map[string]any{ {"toplevel_name": "Company Resources"}, {"name": "Dashboard", "url": "https://example.com/dashboard"}, {"name": "Documentation", "url": "https://example.com/docs"}, { "name": "Tools", "children": []map[string]string{ {"name": "Jira Board", "url": "https://example.com/jira"}, {"name": "Slack", "url": "https://example.com/slack"}, {"name": "GitHub PRs", "url": "https://example.com/github"}, {"name": "Runbooks", "url": "https://example.com/runbooks"}, }, }, }, }, }) if err != nil { panic(err) } _ = pool } ``` ## Updating policies on an existing pool You can update `chrome_policy` on an existing pool. Pass `discard_all_idle: true` to immediately replace all idle browsers with the new policy configuration. ```json theme={null} { "chrome_policy": { "HomepageLocation": "https://kernel.sh", "HomepageIsNewTabPage": false, "ShowHomeButton": true, "NewTabPageLocation": "https://kernel.sh/docs", "RestoreOnStartup": 4, "RestoreOnStartupURLs": ["https://kernel.sh"], "BookmarkBarEnabled": true, "ManagedBookmarks": [ {"toplevel_name": "Company Resources"}, {"name": "Dashboard", "url": "https://example.com/dashboard"}, {"name": "Documentation", "url": "https://example.com/docs"}, { "name": "Tools", "children": [ {"name": "Jira Board", "url": "https://example.com/jira"}, {"name": "Slack", "url": "https://example.com/slack"}, {"name": "GitHub PRs", "url": "https://example.com/github"}, {"name": "Runbooks", "url": "https://example.com/runbooks"} ] } ] }, "discard_all_idle": true } ``` ## Example policies The example above demonstrates setting a default homepage and managed bookmarks. Here's what each policy does: | Policy | Type | Description | | ---------------------- | ---------- | ----------------------------------------------------------------------------------------- | | `HomepageLocation` | `string` | URL loaded when clicking the home button | | `HomepageIsNewTabPage` | `boolean` | When `false`, the home button navigates to `HomepageLocation` instead of the new tab page | | `ShowHomeButton` | `boolean` | Shows the home button in the toolbar | | `NewTabPageLocation` | `string` | URL shown when opening a new tab | | `RestoreOnStartup` | `integer` | Set to `4` to open a specific list of URLs on browser startup | | `RestoreOnStartupURLs` | `string[]` | URLs to open when the browser starts. Requires `RestoreOnStartup` set to `4` | | `BookmarkBarEnabled` | `boolean` | Shows the bookmark bar | | `ManagedBookmarks` | `array` | Pre-configured bookmarks. Supports folders via nested `children` arrays | ## Common use cases The examples below use `browsers.create()`, but the same policies apply when set on a pool. ### Allow pop-ups Chrome's default blocks pop-ups, which breaks sites that open download dialogs or OAuth windows in a new window. Set `DefaultPopupsSetting` to `1` to allow them (`2` blocks). ```python Python theme={null} from kernel import Kernel kernel = Kernel() browser = kernel.browsers.create( chrome_policy={ "DefaultPopupsSetting": 1, } ) ``` ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); const browser = await kernel.browsers.create({ chrome_policy: { DefaultPopupsSetting: 1, }, }); ``` ### Control file download behavior When automating file downloads that trigger permission prompts, combine `DefaultPopupsSetting: 1` with `DownloadRestrictions: 0` to allow all downloads. ```python Python theme={null} from kernel import Kernel kernel = Kernel() browser = kernel.browsers.create( chrome_policy={ "DefaultPopupsSetting": 1, "DownloadRestrictions": 0, } ) ``` ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); const browser = await kernel.browsers.create({ chrome_policy: { DefaultPopupsSetting: 1, DownloadRestrictions: 0, }, }); ``` ### Block DevTools and page source DevTools hands anyone who can reach a browser a console on the page, plus network logs, cookies, and local storage. When you expose a session through live view, or run agent code you don't fully trust, block the URLs DevTools loads from: ```python Python theme={null} from kernel import Kernel kernel = Kernel() browser = kernel.browsers.create( chrome_policy={ "URLBlocklist": ["devtools://*", "chrome://inspect", "view-source:*"], } ) ``` ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); const browser = await kernel.browsers.create({ chrome_policy: { URLBlocklist: ["devtools://*", "chrome://inspect", "view-source:*"], }, }); ``` ### Restrict navigation to specific URLs To lock a browser to an approved set of URLs, block everything with `URLBlocklist` and then allow back only the URLs you want with `URLAllowlist`. Entries match a whole domain (`chatgpt.com`) or a specific path (`en.wikipedia.org/wiki/Cat`), and more specific entries take precedence. This gates top-level navigation, so any other URL returns `ERR_BLOCKED_BY_ADMINISTRATOR`; it does not block resources or API calls a permitted page loads from other origins. ```python Python theme={null} from kernel import Kernel kernel = Kernel() browser = kernel.browsers.create( chrome_policy={ "URLBlocklist": ["*"], "URLAllowlist": ["chatgpt.com", "en.wikipedia.org/wiki/Cat"], } ) ``` ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); const browser = await kernel.browsers.create({ chrome_policy: { URLBlocklist: ["*"], URLAllowlist: ["chatgpt.com", "en.wikipedia.org/wiki/Cat"], }, }); ``` ### Print to PDF `PrintingEnabled` must stay `true` (the default) for the browser's print-to-PDF path — including CDP's `Page.printToPDF` and Playwright's `page.pdf()` — to work. `PrintPdfAsImageDefault` controls whether each page is rasterized to an image before being placed in the PDF, rather than kept as native vector text and graphics: * `false` (shown explicitly below, and Chrome's default) — keeps text selectable and extractable in the output PDF. Use this if automation will read the PDF's contents afterward (text extraction, parsing, LLM ingestion) — image-based PDFs need OCR to get text back out. * `true` — rasterizes every page, which can look more consistent on pages with complex CSS or canvas content, at the cost of searchable text and a larger file. ```python Python theme={null} from kernel import Kernel kernel = Kernel() browser = kernel.browsers.create( chrome_policy={ "PrintingEnabled": True, "PrintPdfAsImageDefault": False, } ) ``` ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); const browser = await kernel.browsers.create({ chrome_policy: { PrintingEnabled: true, PrintPdfAsImageDefault: false, }, }); ``` ## Available policies Any policy listed in the [Chrome Enterprise policy documentation](https://chromeenterprise.google/policies/) can be used in the `chrome_policy` object. Refer to the official docs for the full list of supported policy names, types, and values. # Computer Controls Source: https://kernel.sh/docs/browsers/computer-controls Control the computer's mouse, keyboard, and screen Use OS-level controls to move and click the mouse, type and press keys, scroll, drag, and capture screenshots from a running browser session. Both `moveMouse` and `dragMouse` use human-like [Bézier curves](https://en.wikipedia.org/wiki/B%C3%A9zier_curve) by default. ## Click the mouse Simulate mouse clicks at specific coordinates. You can select the button, click type (down, up, click), number of clicks, and optional modifier keys to hold. ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); const kernelBrowser = await kernel.browsers.create(); // Basic left click at (100, 200) await kernel.browsers.computer.clickMouse(kernelBrowser.session_id, { x: 100, y: 200, }); // Double right-click while holding Shift await kernel.browsers.computer.clickMouse(kernelBrowser.session_id, { x: 100, y: 200, button: 'right', click_type: 'click', num_clicks: 2, hold_keys: ['Shift'], }); ``` ```python Python theme={null} from kernel import Kernel kernel = Kernel() kernel_browser = kernel.browsers.create() # Basic left click at (100, 200) kernel.browsers.computer.click_mouse( id=kernel_browser.session_id, x=100, y=200, ) # Double right-click while holding Shift kernel.browsers.computer.click_mouse( id=kernel_browser.session_id, x=100, y=200, button="right", click_type="click", num_clicks=2, hold_keys=["Shift"], ) ``` ```go Go theme={null} package main import ( "context" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() kernelBrowser, err := client.Browsers.New(ctx, kernel.BrowserNewParams{}) if err != nil { panic(err) } // Basic left click at (100, 200) if err := client.Browsers.Computer.ClickMouse(ctx, kernelBrowser.SessionID, kernel.BrowserComputerClickMouseParams{ X: 100, Y: 200, }); err != nil { panic(err) } // Double right-click while holding Shift if err := client.Browsers.Computer.ClickMouse(ctx, kernelBrowser.SessionID, kernel.BrowserComputerClickMouseParams{ X: 100, Y: 200, Button: kernel.BrowserComputerClickMouseParamsButtonRight, ClickType: kernel.BrowserComputerClickMouseParamsClickTypeClick, NumClicks: kernel.Int(2), HoldKeys: []string{"Shift"}, }); err != nil { panic(err) } } ``` ```bash CLI theme={null} # Click the mouse at coordinates (100, 200) kernel browsers computer click-mouse --x 100 --y 200 # Double-click the right mouse button kernel browsers computer click-mouse --x 100 --y 200 --num-clicks 2 --button right ``` ## Move the mouse Move the cursor to specific screen coordinates. By default, the cursor follows a human-like Bezier curve path instead of teleporting instantly. You can control this with `smooth` and `duration_ms`. | Parameter | Type | Default | Description | | ------------- | ------- | ------- | ---------------------------------------------------------------------------------------------------------- | | `x` | integer | — | X coordinate to move the cursor to | | `y` | integer | — | Y coordinate to move the cursor to | | `smooth` | boolean | `true` | Use human-like Bezier curve path instead of instant teleport | | `duration_ms` | integer | auto | Target duration in milliseconds for smooth movement (50–5000). Omit for automatic timing based on distance | | `hold_keys` | array | — | Modifier keys to hold during the move | ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); const kernelBrowser = await kernel.browsers.create(); // Human-like smooth movement (default) await kernel.browsers.computer.moveMouse(kernelBrowser.session_id, { x: 500, y: 300, }); // Smooth movement with custom duration await kernel.browsers.computer.moveMouse(kernelBrowser.session_id, { x: 800, y: 600, smooth: true, duration_ms: 1500, }); // Instant teleport (disable smooth) await kernel.browsers.computer.moveMouse(kernelBrowser.session_id, { x: 100, y: 200, smooth: false, }); ``` ```python Python theme={null} from kernel import Kernel kernel = Kernel() kernel_browser = kernel.browsers.create() # Human-like smooth movement (default) kernel.browsers.computer.move_mouse( id=kernel_browser.session_id, x=500, y=300, ) # Smooth movement with custom duration kernel.browsers.computer.move_mouse( id=kernel_browser.session_id, x=800, y=600, smooth=True, duration_ms=1500, ) # Instant teleport (disable smooth) kernel.browsers.computer.move_mouse( id=kernel_browser.session_id, x=100, y=200, smooth=False, ) ``` ```go Go theme={null} package main import ( "context" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() kernelBrowser, err := client.Browsers.New(ctx, kernel.BrowserNewParams{}) if err != nil { panic(err) } // Human-like smooth movement (default) if err := client.Browsers.Computer.MoveMouse(ctx, kernelBrowser.SessionID, kernel.BrowserComputerMoveMouseParams{ X: 500, Y: 300, }); err != nil { panic(err) } // Smooth movement with custom duration if err := client.Browsers.Computer.MoveMouse(ctx, kernelBrowser.SessionID, kernel.BrowserComputerMoveMouseParams{ X: 800, Y: 600, Smooth: kernel.Bool(true), DurationMs: kernel.Int(1500), }); err != nil { panic(err) } // Instant teleport (disable smooth) if err := client.Browsers.Computer.MoveMouse(ctx, kernelBrowser.SessionID, kernel.BrowserComputerMoveMouseParams{ X: 100, Y: 200, Smooth: kernel.Bool(false), }); err != nil { panic(err) } } ``` ```bash CLI theme={null} # Smooth movement (default) kernel browsers computer move-mouse --x 500 --y 300 # Instant teleport kernel browsers computer move-mouse --x 500 --y 300 --smooth=false ``` ### Smooth vs instant movement ## Take screenshots Capture a full-screen PNG or a specific region. For WebGL and other animation-heavy pages, `captureScreenshot` is noticeably faster than CDP's `Page.captureScreenshot`. If you're driving a computer-use loop off screenshots, prefer computer controls. [GPU acceleration](/docs/browsers/gpu-acceleration) gives an additional small speedup for computer controls screenshots, but does not speed up CDP screenshots. ```typescript Typescript/Javascript theme={null} import fs from 'fs'; import { Buffer } from 'buffer'; import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); const kernelBrowser = await kernel.browsers.create(); // Full screenshot { const response = await kernel.browsers.computer.captureScreenshot(kernelBrowser.session_id); const blob = await response.blob(); const buffer = Buffer.from(await blob.arrayBuffer()); fs.writeFileSync('screenshot.png', buffer); } // Region screenshot { const response = await kernel.browsers.computer.captureScreenshot(kernelBrowser.session_id, { region: { x: 0, y: 0, width: 800, height: 600 }, }); const blob = await response.blob(); const buffer = Buffer.from(await blob.arrayBuffer()); fs.writeFileSync('region.png', buffer); } ``` ```python Python theme={null} from kernel import Kernel kernel = Kernel() kernel_browser = kernel.browsers.create() # Full screenshot with open('screenshot.png', 'wb') as f: image_data = kernel.browsers.computer.capture_screenshot(id=kernel_browser.session_id) f.write(image_data.read()) # Region screenshot with open('region.png', 'wb') as f: image_data = kernel.browsers.computer.capture_screenshot( id=kernel_browser.session_id, region={"x": 0, "y": 0, "width": 800, "height": 600}, ) f.write(image_data.read()) ``` ```go Go theme={null} package main import ( "context" "io" "os" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() kernelBrowser, err := client.Browsers.New(ctx, kernel.BrowserNewParams{}) if err != nil { panic(err) } // Full screenshot response, err := client.Browsers.Computer.CaptureScreenshot(ctx, kernelBrowser.SessionID, kernel.BrowserComputerCaptureScreenshotParams{}) if err != nil { panic(err) } imageData, err := io.ReadAll(response.Body) closeErr := response.Body.Close() if err != nil { panic(err) } if closeErr != nil { panic(closeErr) } if err := os.WriteFile("screenshot.png", imageData, 0644); err != nil { panic(err) } // Region screenshot response, err = client.Browsers.Computer.CaptureScreenshot(ctx, kernelBrowser.SessionID, kernel.BrowserComputerCaptureScreenshotParams{ Region: kernel.BrowserComputerCaptureScreenshotParamsRegion{ X: 0, Y: 0, Width: 800, Height: 600, }, }) if err != nil { panic(err) } imageData, err = io.ReadAll(response.Body) closeErr = response.Body.Close() if err != nil { panic(err) } if closeErr != nil { panic(closeErr) } if err := os.WriteFile("region.png", imageData, 0644); err != nil { panic(err) } } ``` ```bash CLI theme={null} # Take a full screenshot kernel browsers computer screenshot --to screenshot.png # Take a screenshot of a specific region kernel browsers computer screenshot --to region.png --x 0 --y 0 --width 800 --height 600 ``` ## Type text Type literal text on the host. By default, typing uses human-like variable timing: word-sized chunks, natural pauses at word and sentence boundaries, and optional realistic typos corrected with backspace. Set `smooth: false` for xdotool typing with a fixed per-keystroke delay (`delay`, in ms) or instant input when `delay` is `0`. | Parameter | Type | Default | Description | | ------------- | ------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `text` | string | — | Text to type | | `delay` | integer | `0` | Fixed delay in milliseconds between keystrokes. Only used when `smooth` is `false`; ignored when `smooth` is `true` | | `smooth` | boolean | `true` | Human-like variable keystroke timing with word-boundary pauses (default, same idea as `moveMouse` / `dragMouse`) | | `typo_chance` | number | `0` | Per-character typo rate from 0–0.10 (capped; 0.10 ≈ 10% per character on average), corrected with backspace. Only applies when `smooth` is `true` (silently ignored when `smooth` is `false`). Typical values are 0.02–0.05 | ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); const kernelBrowser = await kernel.browsers.create(); // Human-like smooth typing (default — omit smooth or pass true) await kernel.browsers.computer.typeText(kernelBrowser.session_id, { text: 'The quick brown fox jumps over the lazy dog.', }); // Human-like with occasional typos (3% chance per character) await kernel.browsers.computer.typeText(kernelBrowser.session_id, { text: 'The quick brown fox jumps over the lazy dog.', typo_chance: 0.03, }); // Instant typing (all at once, no per-key delay) await kernel.browsers.computer.typeText(kernelBrowser.session_id, { text: 'Hello, World!', smooth: false, }); // Fixed delay between keystrokes (smooth off) await kernel.browsers.computer.typeText(kernelBrowser.session_id, { text: 'Slow typing...', smooth: false, delay: 100, }); ``` ```python Python theme={null} from kernel import Kernel kernel = Kernel() kernel_browser = kernel.browsers.create() # Human-like smooth typing (default — omit smooth or pass True) kernel.browsers.computer.type_text( id=kernel_browser.session_id, text="The quick brown fox jumps over the lazy dog.", ) # Human-like with occasional typos (3% chance per character) kernel.browsers.computer.type_text( id=kernel_browser.session_id, text="The quick brown fox jumps over the lazy dog.", typo_chance=0.03, ) # Instant typing (all at once, no per-key delay) kernel.browsers.computer.type_text( id=kernel_browser.session_id, text="Hello, World!", smooth=False, ) # Fixed delay between keystrokes (smooth off) kernel.browsers.computer.type_text( id=kernel_browser.session_id, text="Slow typing...", smooth=False, delay=100, ) ``` ```go Go theme={null} package main import ( "context" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() kernelBrowser, err := client.Browsers.New(ctx, kernel.BrowserNewParams{}) if err != nil { panic(err) } // Type literal text if err := client.Browsers.Computer.TypeText(ctx, kernelBrowser.SessionID, kernel.BrowserComputerTypeTextParams{ Text: "The quick brown fox jumps over the lazy dog.", }); err != nil { panic(err) } // Fixed delay between keystrokes if err := client.Browsers.Computer.TypeText(ctx, kernelBrowser.SessionID, kernel.BrowserComputerTypeTextParams{ Text: "Slow typing...", Delay: kernel.Int(100), }); err != nil { panic(err) } } ``` ```bash CLI theme={null} # Human-like smooth typing (default) kernel browsers computer type --text "The quick brown fox" # Human-like with occasional typos kernel browsers computer type --text "The quick brown fox" --typo-chance 0.03 # Instant typing (all at once) kernel browsers computer type --text "Hello, World!" --smooth=false # Fixed delay between keystrokes (smooth off) kernel browsers computer type --text "Slow typing..." --smooth=false --delay 100 ``` ### Smooth vs instant typing ## Press keys Press one or more key symbols (including combinations like "Ctrl+t" or "Ctrl+Shift+Tab"). Optionally hold modifiers and/or set a duration to hold keys down. ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); const kernelBrowser = await kernel.browsers.create(); // Tap a key combination await kernel.browsers.computer.pressKey(kernelBrowser.session_id, { keys: ['Ctrl+t'], }); // Hold keys for 250ms while also holding Alt await kernel.browsers.computer.pressKey(kernelBrowser.session_id, { keys: ['Ctrl+Shift+Tab'], duration: 250, hold_keys: ['Alt'], }); ``` ```python Python theme={null} from kernel import Kernel kernel = Kernel() kernel_browser = kernel.browsers.create() # Tap a key combination kernel.browsers.computer.press_key( id=kernel_browser.session_id, keys=["Ctrl+t"], ) # Hold keys for 250ms while also holding Alt kernel.browsers.computer.press_key( id=kernel_browser.session_id, keys=["Ctrl+Shift+Tab"], duration=250, hold_keys=["Alt"], ) ``` ```go Go theme={null} package main import ( "context" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() kernelBrowser, err := client.Browsers.New(ctx, kernel.BrowserNewParams{}) if err != nil { panic(err) } // Tap a key combination if err := client.Browsers.Computer.PressKey(ctx, kernelBrowser.SessionID, kernel.BrowserComputerPressKeyParams{ Keys: []string{"Ctrl+t"}, }); err != nil { panic(err) } // Hold keys for 250ms while also holding Alt if err := client.Browsers.Computer.PressKey(ctx, kernelBrowser.SessionID, kernel.BrowserComputerPressKeyParams{ Keys: []string{"Ctrl+Shift+Tab"}, Duration: kernel.Int(250), HoldKeys: []string{"Alt"}, }); err != nil { panic(err) } } ``` ```bash CLI theme={null} # Press one or more keys (repeatable --key) kernel browsers computer press-key --key Ctrl+t # Hold for a duration and add optional modifiers kernel browsers computer press-key --key Ctrl+Shift+Tab --duration 250 --hold-key Alt ``` ## Scroll Scroll the mouse wheel at a specific position. Positive `delta_y` scrolls down; negative scrolls up. Positive `delta_x` scrolls right; negative scrolls left. Scroll amounts refer to "wheel units." ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); const kernelBrowser = await kernel.browsers.create(); await kernel.browsers.computer.scroll(kernelBrowser.session_id, { x: 300, y: 400, delta_x: 0, delta_y: 120, }); ``` ```python Python theme={null} from kernel import Kernel kernel = Kernel() kernel_browser = kernel.browsers.create() kernel.browsers.computer.scroll( id=kernel_browser.session_id, x=300, y=400, delta_x=0, delta_y=120, ) ``` ```go Go theme={null} package main import ( "context" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() kernelBrowser, err := client.Browsers.New(ctx, kernel.BrowserNewParams{}) if err != nil { panic(err) } if err := client.Browsers.Computer.Scroll(ctx, kernelBrowser.SessionID, kernel.BrowserComputerScrollParams{ X: 300, Y: 400, DeltaX: kernel.Int(0), DeltaY: kernel.Int(120), }); err != nil { panic(err) } } ``` ```bash CLI theme={null} # Scroll at a position kernel browsers computer scroll --x 300 --y 400 --delta-y 120 ``` ## Drag the mouse Drag by pressing a button, moving along a path of points, then releasing. By default, drag movement uses human-like Bezier curves between waypoints. Set `smooth: false` to use linear interpolation with `steps_per_segment` and `step_delay_ms` instead. | Parameter | Type | Default | Description | | ------------------- | ------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `path` | array | — | Ordered list of `[x, y]` coordinate pairs to move through (minimum 2 points) | | `button` | string | `left` | Mouse button: `left`, `middle`, or `right` | | `smooth` | boolean | `true` | Use human-like Bezier curves between waypoints. When `true`, `steps_per_segment` and `step_delay_ms` are ignored | | `duration_ms` | integer | auto | Target duration in milliseconds for the entire drag when `smooth=true` (50–10000). Omit for automatic timing based on total path length | | `delay` | integer | `0` | Delay in milliseconds between button down and starting to move | | `steps_per_segment` | integer | `10` | Number of interpolation steps per path segment (only when `smooth=false`) | | `step_delay_ms` | integer | `50` | Delay in milliseconds between steps (only when `smooth=false`) | | `hold_keys` | array | — | Modifier keys to hold during the drag | ### Smooth vs linear drag ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); const kernelBrowser = await kernel.browsers.create(); // Human-like smooth drag (default) await kernel.browsers.computer.dragMouse(kernelBrowser.session_id, { path: [ [100, 200], [400, 350], [700, 200], ], }); // Smooth drag with custom duration await kernel.browsers.computer.dragMouse(kernelBrowser.session_id, { path: [ [100, 200], [400, 350], [700, 200], ], smooth: true, duration_ms: 2000, }); // Linear interpolation drag (legacy behavior) await kernel.browsers.computer.dragMouse(kernelBrowser.session_id, { path: [ [100, 200], [400, 350], [700, 200], ], smooth: false, steps_per_segment: 10, step_delay_ms: 50, }); ``` ```python Python theme={null} from kernel import Kernel kernel = Kernel() kernel_browser = kernel.browsers.create() # Human-like smooth drag (default) kernel.browsers.computer.drag_mouse( id=kernel_browser.session_id, path=[[100, 200], [400, 350], [700, 200]], ) # Smooth drag with custom duration kernel.browsers.computer.drag_mouse( id=kernel_browser.session_id, path=[[100, 200], [400, 350], [700, 200]], smooth=True, duration_ms=2000, ) # Linear interpolation drag (legacy behavior) kernel.browsers.computer.drag_mouse( id=kernel_browser.session_id, path=[[100, 200], [400, 350], [700, 200]], smooth=False, steps_per_segment=10, step_delay_ms=50, ) ``` ```go Go theme={null} package main import ( "context" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() kernelBrowser, err := client.Browsers.New(ctx, kernel.BrowserNewParams{}) if err != nil { panic(err) } // Human-like smooth drag (default) if err := client.Browsers.Computer.DragMouse(ctx, kernelBrowser.SessionID, kernel.BrowserComputerDragMouseParams{ Path: [][]int64{ {100, 200}, {400, 350}, {700, 200}, }, }); err != nil { panic(err) } // Smooth drag with custom duration if err := client.Browsers.Computer.DragMouse(ctx, kernelBrowser.SessionID, kernel.BrowserComputerDragMouseParams{ Path: [][]int64{ {100, 200}, {400, 350}, {700, 200}, }, Smooth: kernel.Bool(true), DurationMs: kernel.Int(2000), }); err != nil { panic(err) } // Linear interpolation drag (legacy behavior) if err := client.Browsers.Computer.DragMouse(ctx, kernelBrowser.SessionID, kernel.BrowserComputerDragMouseParams{ Path: [][]int64{ {100, 200}, {400, 350}, {700, 200}, }, Smooth: kernel.Bool(false), StepsPerSegment: kernel.Int(10), StepDelayMs: kernel.Int(50), }); err != nil { panic(err) } } ``` ```bash CLI theme={null} # Smooth drag (default) kernel browsers computer drag-mouse \ --point 100,200 \ --point 400,350 \ --point 700,200 # Linear interpolation drag kernel browsers computer drag-mouse \ --point 100,200 \ --point 400,350 \ --point 700,200 \ --smooth=false \ --steps-per-segment 10 \ --step-delay-ms 50 ``` # Curl Source: https://kernel.sh/docs/browsers/curl Send HTTP requests through Kernel browsers Browser curl lets you run HTTP requests through Kernel browsers, automatically attaching the browser's cookie jar, transport fingerprint, [stealth and proxy settings](/docs/browsers/bot-detection/overview), and other defaults that browsers send. This allows you to efficiently fetch resources from websites while getting the benefits of real browsers, including bot anti-detection and session cookie state. ## Why use browser curl instead of `fetch()` When you call `fetch()` or `httpx`, you're on a different TLS stack, IP, cookie store, and header profile than the browsers your agents (or automations) are using. Browser curl runs requests directly through a Kernel browser's networking stack. ### What curl requests inherit from Kernel browsers * **Cookies and storage policy** — Same cookie jar as the profile, including Chromium inclusion rules, `Set-Cookie` persistence, session cookies, encryption, `SameSite`, partitioning, and content settings. Response `Set-Cookie` headers update that same profile. * **Transport fingerprint** — BoringSSL TLS, ALPN, HTTP/2, Alt-Svc, connection reuse, Happy Eyeballs, certificate verification, CT/HSTS policy. * **Network configuration** — Proxy and PAC settings, DNS and Secure DNS, SSL and cert policy, cache, HTTP server properties, network quality signals. * **Default client behavior** — Chromium user agent, `Accept-Language`, brotli/zstd support, and typical fetch metadata behavior. * **Chromium request lifecycle** — Browser-managed redirects, decompression, HTTP authentication and client certificate eligibility, and cache behavior. ## Streaming browser requests Streaming requests go through the browser's Chromium network stack, and the SDK mirrors platform-native HTTP ergonomics. You can stream or read the body incrementally, which avoids buffering the full payload in the browser for large responses. ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); const browser = await kernel.browsers.create({}); const response: Response = await kernel.browsers.fetch(browser.session_id, 'https://example.com', { method: 'GET', }); console.log('body', await response.text()); ``` ```python Python theme={null} import httpx from kernel import Kernel client = Kernel() browser = client.browsers.create() response: httpx.Response = client.browsers.request(browser.session_id, "GET", "https://example.com") print("status", response.status_code) ``` ```go Go theme={null} package main import ( "context" "fmt" "io" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() browser, err := client.Browsers.New(ctx, kernel.BrowserNewParams{}) if err != nil { panic(err) } httpClient, err := client.Browsers.HTTPClient(browser.SessionID) if err != nil { panic(err) } response, err := httpClient.Get("https://example.com") if err != nil { panic(err) } defer response.Body.Close() body, err := io.ReadAll(response.Body) if err != nil { panic(err) } fmt.Println("body", string(body)) } ``` ## Buffered browser curl Buffered browser curl calls the HTTP API and returns a single JSON envelope: `status`, `headers`, `body`, and `duration_ms`. Use it when the response is small enough to hold in memory and you want one structured object back—typical APIs, HTML snippets, and JSON payloads. Buffered curl loads the **entire** response body into the browser process before it returns to you. Requesting a very large payload can exhaust memory and cause OOMs. For big downloads or unknown sizes, use [streaming browser requests](#streaming-browser-requests) instead. ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); const browser = await kernel.browsers.create({}); const buffered = await kernel.browsers.curl(browser.session_id, { url: 'https://example.com', method: 'GET', }); console.log('body', buffered.body); ``` ```python Python theme={null} from kernel import Kernel client = Kernel() browser = client.browsers.create() buffered = client.browsers.curl(browser.session_id, url="https://example.com", method="GET") print("body", buffered.body) ``` ```go Go theme={null} package main import ( "context" "fmt" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() browser, err := client.Browsers.New(ctx, kernel.BrowserNewParams{}) if err != nil { panic(err) } buffered, err := client.Browsers.Curl(ctx, browser.SessionID, kernel.BrowserCurlParams{ URL: "https://example.com", Method: kernel.BrowserCurlParamsMethodGet, }) if err != nil { panic(err) } fmt.Println("body", buffered.Body) } ``` ## Concurrency limits Browser curl concurrency is constrained by Chromium's internal networking limits: * **HTTP/1.x (direct)** — About 6 sockets per host group and up to 256 active sockets per connection pool. * **Proxied chains** — On the order of tens of sockets per proxy chain (Chromium clamps configured values into a bounded range). * **HTTP stream pool** — Similar per-group and per-pool behavior to HTTP/1.x direct connections. * **HTTP/2** — Roughly 100 concurrent streams per session initially, updated from the server's `SETTINGS_MAX_CONCURRENT_STREAMS`, with an upper cap in Chromium (on the order of 256). If you're issuing many parallel curls from one browser, you're sharing those connection pools with navigation, XHR, and other session traffic. Latency may vary, as Chromium may queue requests when it reaches its limits. # Enable Payments in a Browser Agent Source: https://kernel.sh/docs/browsers/enable-payments-in-browser-agent Give a browser agent provider-backed payment aliases and complete a web checkout without exposing card data you're building a browser agent. you can add payments without bringing your application, agent, or browser into pci dss scope. create a KERNEL vault and add a wallet backed by [stripe link](/docs/integrations/payments/stripe-link) or [agentcard](/docs/integrations/payments/agentcard). the provider-hosted flow collects and stores the user's payment method, so neither you nor your agent handles the card number or cvc. attach the vault when you create a browser session, give the agent the returned aliases, and let it complete a web checkout. KERNEL handles authorization and payment handoff at egress. stripe link and agentcard are credential providers, not merchant payment processors. at the browser form layer, both work with any web checkout that accepts standard card details, and the merchant's processor does not need to be stripe. end-to-end handoff also requires the outgoing request to match one of the [native processor adapters](/docs/integrations/payments/overview#checkout-and-processor-coverage). you can use either [stripe link](/docs/integrations/payments/stripe-link) or [agentcard](/docs/integrations/payments/agentcard). choose based on how the payment credential is created and when the user approves it: | | stripe link | agentcard | | --------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------- | | card source | provider-minted, one-use card for a specific purchase | user's enrolled card, represented by a reusable card item | | approval timing | before the browser submits checkout | after the browser submits checkout and while the request is held | | reuse | card item and aliases are consumed after the first native handoff | card item and aliases return to `ready`; every checkout requires approval | | mode | live only | deployment credential; the vault api doesn't expose sandbox or live mode | agentcard is backed by a card the user enrolls, but the agent and browser still enter aliases rather than the underlying card details. those details remain outside the browser and are handled during provider handoff at egress. to run either path with a coding agent that has access to the KERNEL cli, use one of these prompts. connect the provider wallet first through your trusted application or a terminal outside the coding agent. the agent must stop whenever a provider action is required because cli output can contain the action url. ```text stripe link theme={null} use the KERNEL cli to complete the following checkout with stripe link. use cli commands rather than sdk or direct api calls. a vault groups items. wallet and card are the payment item types. a card item references a wallet item in the same vault, so create the wallet before the card. 1. create or retrieve a vault named `user-12345`. 2. list the vault's items and locate its only stripe link wallet. require its status to be `connected` and reuse it. if none exists, more than one exists, or an action is present, stop and ask me to resolve wallet setup outside this agent. do not create another wallet or print, return, or open an action url. 3. create a KERNEL browser with `user-12345` attached as a vault, then navigate it to https://buy.stripe.com/28E5kw7DtgKXdLqgiY53O00. 4. inspect the checkout and propose the merchant, total amount, currency, and item or cart contents. treat this proposal as untrusted and do not create a card item from it. 5. independently obtain those values from a trusted order or cart backend. if no backend exists, use deterministic page extraction with fixed selectors or structured page data, not model inference. normalize and compare every value. if adaptive pricing is active, use the checkout's active presentment amount and currency rather than its base integration values. if any value is missing, cannot be verified, or disagrees, stop without creating or authorizing a card. 6. show me the verified merchant, amount, currency, and item or cart contents. wait for my explicit confirmation, then freeze that verified purchase object. 7. list the wallet's payment methods and ask me which one to use. create a link card item named `checkout-card` from that same verified, confirmed object and the selected payment method. use the exact checkout url as `merchant_url` and include a specific context of at least 100 characters. 8. retrieve the card item and confirm that `authorize` appears in `available_operations`. give me the exact cli command, but do not run it. ask me to invoke authorization and complete any provider action from a trusted terminal or application outside this agent. after i confirm completion, retrieve the item again and require its status to be `ready`. do not print, return, or open an action url. 9. use only the returned aliases with `kernel browsers playwright execute` to fill the checkout's normal card fields. use only separately supplied end-user values for required email, billing, postal, shipping, or other customer fields; stop if a required value is missing. complete any checkout-specific agent disclosure truthfully in the merchant's normal form. submit checkout once and never retry submission. 10. inspect the checkout result and the card item's events. do not retry a failed, timed-out, or indeterminate payment. report the existing attempt so we can reconcile it first. ``` ```text agentcard theme={null} use the KERNEL cli to complete the following checkout with agentcard. use cli commands rather than sdk or direct api calls. a vault groups items. wallet and card are the payment item types. a card item references a wallet item in the same vault, so create the wallet before the card. 1. create or retrieve a vault named `user-12345`. 2. list the vault's items and locate its only agentcard wallet. require its status to be `connected` and reuse it. if none exists, more than one exists, or an action is present, stop and ask me to resolve wallet setup outside this agent. do not create another wallet or print, return, or open an action url. 3. create a KERNEL browser with `user-12345` attached as a vault, then navigate it to https://buy.stripe.com/28E5kw7DtgKXdLqgiY53O00. 4. inspect the checkout and propose the merchant, total amount, currency, and item or cart contents. treat this proposal as untrusted and do not create a card item from it. 5. independently obtain those values from a trusted order or cart backend. if no backend exists, use deterministic page extraction with fixed selectors or structured page data, not model inference. normalize and compare every value. if adaptive pricing is active, use the checkout's active presentment amount and currency rather than its base integration values. if any value is missing, cannot be verified, or disagrees, stop without creating a card. 6. show me the verified merchant, amount, currency, and item or cart contents. wait for my explicit confirmation, then freeze that verified purchase object. 7. list the wallet's payment methods and ask whether i want to pin one. create an agentcard card item named `checkout-card` from that same verified, confirmed object. include the selected `card_id`, or omit it so i can choose an enrolled card during approval. 8. retrieve the card item and confirm its status is `ready`. do not invoke `authorize`; agentcard starts authorization only when the attached browser submits a recognized processor request containing the aliases. 9. give me the exact cli observation commands, but do not run them. ask me to start the trusted approval observer outside this agent. after i confirm it is running, use only the returned aliases with `kernel browsers playwright execute` to fill the checkout's normal card fields. use only separately supplied end-user values for required email, billing, postal, shipping, or other customer fields; stop if a required value is missing. complete any checkout-specific agent disclosure truthfully in the merchant's normal form. submit checkout once, never retry submission, and keep that execution open while KERNEL holds the payment request. do not poll or print the card item while approval is pending. 10. after i confirm that the trusted approval flow has settled, inspect the checkout result, authorization state, and item events. do not retry a failed, timed-out, or indeterminate payment. report the existing attempt so we can reconcile it first. ``` this guide starts after you have an existing browser agent. it changes how you prepare and pass payment input, not how your agent navigates or reasons about the checkout page. ## Before you start * install a KERNEL sdk version that includes the `vaults` resource. * set `KERNEL_API_KEY` and `KERNEL_PROJECT_ID` in the trusted controller that creates your browser. * use a low-value web checkout you control whose outgoing payment request matches a [native processor adapter](/docs/integrations/payments/overview#checkout-and-processor-coverage). the merchant processor doesn't need to be stripe. * stripe link card creation is live-only. agentcard mode comes from the integration's configured credential. * for agentcard, keep an application-owned `AGENTCARD_MODE` deployment setting and fail closed unless it explicitly matches the sandbox or live environment you intend to use. the vault api does not return this mode. keep wallet collection and payment approval outside the agent. show hosted action urls only in a trusted user-facing surface. the only payment values you give the agent are the aliases returned in card state. ## 1. Create a vault scope the client and vault to the same project that will own the browser session. ```typescript TypeScript theme={null} import Kernel from "@onkernel/sdk"; const projectID = process.env.KERNEL_PROJECT_ID; if (!projectID) throw new Error("set KERNEL_PROJECT_ID"); const kernel = new Kernel({ projectID, maxRetries: 0 }); const vault = await kernel.vaults.upsert({ name: "user-12345" }); ``` ```python Python theme={null} import os from kernel import Kernel kernel = Kernel( project_id=os.environ["KERNEL_PROJECT_ID"], max_retries=0, ) vault = kernel.vaults.upsert(name="user-12345") ``` ```bash CLI theme={null} kernel vaults create --name user-12345 ``` `vaults.upsert` creates the vault or retrieves an existing vault with the same name. vault names are immutable within the project. ## 2. Connect the end user's payment method and prepare a card item `wallet` and `card` are KERNEL api resources, not the underlying wallet or card. your end user initiates setup and purchases through your product. your application then makes the authenticated api calls on their behalf. a card item represents the payment authorization and aliases for a purchase; creating one does not add or expose the end user's real card. the end user creates the purchase intent. your application translates that confirmed intent into a KERNEL card item without exposing its api key to the user or agent. | actor | responsibility | | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | your application | enforces one wallet per provider; independently verifies purchase values; calls KERNEL; presents provider actions; observes state; attaches the vault | | your end user | initiates wallet setup and purchases; adds or selects the real payment method; confirms the verified purchase; completes approval | | your browser agent | proposes purchase values; after verification and confirmation, receives aliases, enters them into the checkout form, and submits once | | KERNEL | creates the requested vault resources, returns aliases, enforces browser bindings, and handles payment data at egress | | the credential provider | hosts wallet connection, enrollment, and approval; stores or mints the underlying payment credential | | the merchant's processor | receives the recognized checkout request and returns its tokenization or payment response | ### Once per end user and provider your payment settings ui must allow at most one wallet item for each provider in a vault. list the vault's items before rendering provider controls, group wallet items by `spec.provider`, and apply this flow: | existing wallet for the provider | ui behavior | | -------------------------------- | ------------------------------------------------------------------------------------------------------ | | none | show the option to connect that provider | | `pending_authorization` | hide the add option and resume the existing hosted action | | `connected` | hide the add option, show the provider as connected, and reuse the existing wallet | | any other state | hide the add option and show the existing state; recover it or use an explicit remove-and-replace flow | if both stripe link and agentcard wallets exist, hide both provider add options. recheck this condition in your trusted controller immediately before wallet creation so a stale ui cannot submit a second wallet. the api currently makes item keys unique, not wallet providers. using a different key can create another wallet for the same provider, so your product must enforce the one-per-provider rule. deleting a wallet invalidates its dependent card items; require an explicit replacement flow rather than creating a second wallet beside it. 1. your end user chooses a provider that does not already have a wallet in the vault. 2. your application creates or retrieves a vault for that end user, checks its items again, then creates a wallet item only when that provider is absent. 3. your application presents the returned action in a trusted user-facing surface outside the agent-controlled browser. 4. your end user completes the provider-hosted connection or enrollment flow. 5. your application waits until the wallet item's status is `connected`. this is the point where your end user adds or connects their real payment method. the end user does not create the KERNEL wallet item directly. ### Present hosted actions in your application provider action urls are bearer-like handoffs to enrollment or approval. route them through your trusted application: 1. your backend retrieves the item and keeps the raw action url out of logs, analytics, and model context. 2. store the action server-side under an opaque id bound to the authenticated end user, vault id, item key, and action name. 3. render a link to your own authenticated action endpoint. before redirecting, verify the session owns that binding and the item still returns the same action. 4. send the redirect with `Cache-Control: no-store` and `Referrer-Policy: no-referrer`. 5. apply a short application ttl capped by `item.expires_at` or `state.authorization.expires_at` when present. invalidate the record immediately when the action changes, disappears, or reaches a terminal state. the `presentProviderAction` functions later in this guide represent this application-owned flow. the checkout agent and its browser must never receive the raw provider url. ### For each purchase purchase verification is a required, fail-closed gate before creating or updating a card item: 1. let the browser agent propose the merchant, amount, currency, and item or cart contents. treat every proposed value as untrusted. 2. independently obtain the expected values from a trusted source. prefer your order or cart backend. when no backend exists, use deterministic page extraction with fixed selectors or structured page data, not another model response. 3. normalize the values in trusted code and compare the proposal with the trusted result. compare the amount in minor currency units and require the merchant, currency, and item or cart contents to match. 4. stop when any value is missing, cannot be verified, or disagrees. do not create or update a card item and do not invoke authorization. 5. show the independently verified values to the end user and wait for explicit confirmation. 6. freeze that verified, confirmed purchase object. derive the card specification and any authorization request from that same object. do not accept replacement values from the agent after confirmation. 7. for stripe link, list the connected wallet's payment methods and let the end user select one. for agentcard, pin an enrolled card or let the end user choose during approval. 8. ask KERNEL to prepare a card item on the end user's behalf. stripe link requires a new one-use item. agentcard can reuse a ready item and update its specification when the api permits. the item refers to the connected wallet; it is not the end user's real card. 9. for stripe link, invoke the advertised `authorize` operation using the same verified purchase object, then present any returned approval action to the end user. agentcard begins authorization after the browser submits checkout. 10. wait for the card item to become `ready`, then pass only its aliases to the browser agent. never create or authorize a card from values supplied only by the browser agent. end-user confirmation does not make an unverified proposal trustworthy. the independently verified purchase object must be the single source for the card specification, approval display, and authorization. purchase verification doesn't depend on the merchant processor. prefer the merchant's trusted order or cart backend. if one isn't available, use documented structured checkout data or deterministic extraction for that checkout, and fail closed when the values can't be independently verified. for a stripe payment link without an order or cart backend, use the payment-link response as the deterministic source. the current response exposes `account_settings.display_name`, `line_item_group.total`, `line_item_group.currency`, and `line_item_group.line_items`. use dom text and `data-testid` attributes only as supplemental checks. stripe can render multiple responsive copies of a summary or omit product-level test ids in another layout, so don't require a specific test id or number of matching elements. these are stripe page details rather than a KERNEL contract. if the structured response is missing or its values disagree with the rendered checkout, fail verification instead of falling back to model inference. stripe adaptive pricing can change the checkout's displayed amount and currency for the browser's location. create the card item from the active presentment amount and currency shown to the user and submitted by that checkout, not the payment link's base integration amount and currency. include those active values in the verified purchase object and the confirmation screen. when the checkout response is your deterministic source, create the attached browser in step 3 before you create or update the card item, then inspect and submit with that same browser session. the vault attachment covers items created later in that vault. don't start checkout until the wallet is `connected` and the card item is `ready`. without a wallet item, card creation fails because `spec.wallet` must reference a wallet from the same vault and provider. with an unconnected link wallet, card creation returns a conflict. with an unconnected agentcard wallet, a card without `card_id` can remain `requested`, while a pinned `card_id` cannot be validated. neither path is ready for checkout. we're adding credential-provider integrations and merchant-processor adapters and will update this guide as coverage expands. we plan to build additional card-collection and vault-management surfaces so developers can connect the credential providers and third-party vaults they already use. over time, we want KERNEL to support more of the browser-side steps required to complete a payment without exposing the underlying credential to the agent or browser. both credential-provider flows leave you with a card item whose `state.status` is `ready` and whose `state.aliases` contains `number`, `cvc`, `exp_month`, and `exp_year`. ### Collect non-card checkout fields payment aliases cover only the card number, cvc, and expiry. merchant checkouts can also require email, billing name, postal code, shipping address, phone number, or other customer data. collect the required values from the end user in your trusted application, or load values the end user has already approved from your backend. pass them to the browser agent separately from the card aliases. do not ask the agent to invent missing customer data. merchant-specific agent disclosures are normal checkout fields. if the page asks whether automation is acting for another person, instruct the agent to answer truthfully in the merchant's form before submission. for a checkout that offers **I am an AI agent acting on behalf of someone else**, select that option. stripe can render hidden or duplicate copies of its disclosure control for responsive layouts. target the visible label. if the label doesn't toggle the control, locate the associated real `input[type="checkbox"]` and invoke its native dom `click()`. read that same input's `checked` property and require it to be `true` before submission. if you can't verify the checked state, stop without submitting. don't bypass the disclosure or replace the page's normal submission with a raw processor request. after triggering submission once, never retry it, including after a timeout, unchanged page, or indeterminate result. retrieve the item immediately before creating the browser. don't cache aliases after an item expires, changes state, or is deleted. ```typescript TypeScript theme={null} const card = await kernel.vaults.items.retrieve("notebook-order", { id_or_name: vault.id, wait: 60, }); if ( card.type !== "card" || card.state.status !== "ready" || !card.state.aliases ) { throw new Error(`payment item is ${card.state.status}`); } const aliases = card.state.aliases; ``` ```python Python theme={null} card = kernel.vaults.items.retrieve( "notebook-order", id_or_name=vault.id, wait=60, ) if card.type != "card" or card.state.status != "ready" or card.state.aliases is None: raise RuntimeError(f"payment item is {card.state.status}") aliases = card.state.aliases ``` ```bash CLI theme={null} kernel vaults items get user-12345 notebook-order --wait 60 -o json ``` ## 3. Attach the vault to the browser vault attachments are fixed at browser creation. use the same project-scoped client that created the vault. ```typescript TypeScript theme={null} const browser = await kernel.browsers.create({ vaults: [{ id: vault.id }], headless: false, timeout_seconds: 1800, }); if (!browser.browser_live_view_url) { throw new Error("headful browser did not return a live view url"); } await presentLiveView({ userID: authenticatedUser.id, sessionID: browser.session_id, url: browser.browser_live_view_url, }); ``` ```python Python theme={null} browser = kernel.browsers.create( vaults=[{"id": vault.id}], headless=False, timeout_seconds=1800, ) if browser.browser_live_view_url is None: raise RuntimeError("headful browser did not return a live view url") present_live_view( user_id=authenticated_user.id, session_id=browser.session_id, url=browser.browser_live_view_url, ) ``` ```bash CLI theme={null} kernel browsers create --vault user-12345 -o json ``` `browser_live_view_url` lets the end user watch the checkout during confirmation and agentcard approval pauses. `presentLiveView` represents your authenticated application page: keep the url server-side with the end user and browser session binding, render or embed it only after checking that session, and remove it when you delete or time out the browser. don't log the url or put it in model context. see [live view](/docs/browsers/live-view#embedding-in-an-iframe) for iframe and csp requirements. connect your existing agent to `browser.cdp_ws_url`. see [Controlling a Browser](/docs/introduction/control) for supported connection options. ## 4. Give the aliases to your agent pass the aliases as structured task input. instruct the agent to use them in the page's normal card fields and submit checkout once. ```text theme={null} Complete the checkout at https://shop.example.com for one notebook. The approved total is 23.06 USD. Use this payment input in the checkout form: - card number: - cvc: - expiry month: - expiry year: Use these separately collected customer fields where the checkout requires them: - email: - billing name: - postal code: If the checkout asks whether an agent is acting for another person, select the truthful disclosure option in the page before submission. For example: - I am an AI agent acting on behalf of someone else Submit the checkout once and never retry submission. If the payment request pauses for user approval, wait for the controller to finish that approval. Do not retry payment. ``` replace the angle-bracketed fields in your controller before sending the task. don't put oauth codes, action urls, provider responses, the vault api key, or the browser connection url in the prompt. the agent can fill both top-level fields and payment fields embedded in iframes. let the merchant page's own submission code create the outgoing request. don't replace the checkout with a raw processor api call. ## 5. Keep approval and observation outside the agent start observing the card item before the agent submits checkout and keep the observer running until the merchant reaches a terminal order state. this is required for agentcard, where the outgoing checkout request is held while the user approves it. the same observer works for stripe link and provides item events after credential substitution. the example uses `presentProviderAction`, an application-owned function. it must publish the action only to an authenticated session for the end user who owns the vault. bind the action to that user, vault, and item; expire it no later than the item or authorization expiry; and stop serving it as soon as the action changes or disappears. don't put the url in application logs, analytics, model context, or the agent-controlled browser. ```typescript TypeScript theme={null} let after: string | undefined; async function observePayment(stop: AbortSignal): Promise { while (!stop.aborted) { const current = await kernel.vaults.items.retrieve(card.key, { id_or_name: vault.id, wait: 5, }); if (current.action && "url" in current.action) { await presentProviderAction({ userID: authenticatedUser.id, vaultID: vault.id, item: current, }); } else if (current.action?.name === "push_approval") { console.log("complete the approval in your wallet"); } const events = await kernel.vaults.items.events(card.key, { id_or_name: vault.id, after, wait: 5, }); for (const event of events) { console.log(event.id, event.name, event.browser_id, event.data); after = event.id; } } } ``` ```python Python theme={null} from threading import Event def observe_payment(stop: Event) -> None: after = None while not stop.is_set(): current = kernel.vaults.items.retrieve( card.key, id_or_name=vault.id, wait=5, ) if current.action is not None and hasattr(current.action, "url"): present_provider_action( user_id=authenticated_user.id, vault_id=vault.id, item=current, ) elif current.action is not None and current.action.name == "push_approval": print("complete the approval in your wallet") if after is None: events = kernel.vaults.items.events(card.key, id_or_name=vault.id, wait=5) else: events = kernel.vaults.items.events( card.key, id_or_name=vault.id, after=after, wait=5, ) for event in events: print(event.id, event.name, event.browser_id, event.data) after = event.id ``` ```bash CLI theme={null} kernel vaults items get user-12345 notebook-order --wait 5 --open kernel vaults items events user-12345 notebook-order --wait 60 -o json ``` start the observer before checkout and cancel it only after the checkout controller reaches a terminal merchant state or its reconciliation deadline. the following application-owned functions stand in for your existing agent and order backend: ```typescript TypeScript theme={null} const stop = new AbortController(); const observer = observePayment(stop.signal); try { await runBrowserAgentCheckout({ browser, aliases, verifiedPurchase }); await waitForMerchantResolution(verifiedPurchase.orderID); } finally { stop.abort(); try { await observer; } finally { await kernel.browsers.deleteByID(browser.session_id); } } ``` ```python Python theme={null} from threading import Event, Thread stop = Event() observer = Thread(target=observe_payment, args=(stop,)) observer.start() try: run_browser_agent_checkout( browser=browser, aliases=aliases, verified_purchase=verified_purchase, ) wait_for_merchant_resolution(verified_purchase.order_id) finally: stop.set() try: observer.join() finally: kernel.browsers.delete_by_id(browser.session_id) ``` if the merchant has not reached a terminal state by your controller's deadline, classify the attempt as indeterminate, stop the observer, retain the attempt identifiers, and do not submit checkout again. the observer presents user actions and reads events; it does not submit checkout or repeat authorization. each cli `--wait` performs one bounded observation, so rerun the commands to continue observing the existing attempt. CLI commands prepare and observe the payment state; they don't submit merchant payments or retry them. run `--open` only from the trusted controller or a human-operated terminal, and never send its output to the browser agent. `events` returns an ordered array. an empty array means no new observation arrived during the wait. it does not mean payment failed. correlate `event.browser_id` with the browser you created when a vault is attached to more than one session. ## 6. Verify the outcome use item state, item events, the checkout page, and the merchant's order record together. | observation | next action | | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | | `payment_succeeded` | verify that the merchant created the expected order | | `payment_requires_action` | continue the existing merchant flow without resubmitting payment | | `payment_processing` | wait for the existing payment and inspect the merchant's status | | stripe link `consumed`, `credential_submitted`, or `credential_tokenized` | confirm processor and merchant state; credential use alone does not prove purchase success | | agentcard `ready` or authorization `approved` | inspect charge, replay, and merchant state; reusable item state does not prove purchase success | | decline, expiry, rejection, failure, abandonment, or `payment_unknown` | stop and reconcile the existing attempt before deciding whether a new purchase is appropriate | use the merchant order record as the authority for whether the expected order was created and paid. require its merchant, amount, currency, and items to match the frozen purchase object. use authorization state and item events to explain the provider and handoff path, and use the checkout page as supporting evidence. only report success when the trusted merchant record confirms the matching order. a ready agentcard item, an approved authorization, a delivered replay, or a success page cannot establish that by itself. merchant success text is page-specific: a completed stripe checkout might say **Thanks for your payment**, but don't use that string or any other generic text matcher as the success condition. when the sources disagree or the merchant record is unavailable, keep the result indeterminate and do not retry. a timeout, browser deletion, missing event, or closed checkout does not undo provider execution or cancel a merchant order. don't retry automatically. retain the vault id, card key, browser id, and last event id until you have reconciled the attempt. delete the browser when the task and outcome inspection are complete. keep or delete the vault and provider items according to whether the user needs them for a future task. # Extensions Source: https://kernel.sh/docs/browsers/extensions Use browser extensions in Kernel browsers Kernel's browsers support running with custom Chrome extensions. Chrome extensions must be unpacked and can be uploaded to Kernel via the CLI or API. ## Uploading extensions Here is a simple example of an unpacked extension: ```js ./my-extension/content-script.js theme={null} document.body.innerHTML = document.body.innerHTML.replace(/AI/g, "A1"); ``` ```json ./my-extension/manifest.json theme={null} { "manifest_version": 3, "version": "1.0", "name": "AI to A1", "description": "Replace AI with A1", "content_scripts": [ { "matches": [ "https://*/*" ], "js": [ "content-script.js" ] } ] } ``` Once these files are in place, you can upload them to Kernel via the CLI (or [API](https://kernel.sh/docs/api-reference/extensions/upload-a-browser-extension)): ```bash theme={null} kernel extensions upload ./my-extension --name my-extension ``` Extensions uploaded to Kernel are assigned a random ID, but you can also give them a name for easier reference. This name must be unique within your [project](/docs/info/projects). To retrieve an extension's metadata without downloading the archive, call `GET /extensions/{id_or_name}/metadata`. The response includes the extension's ID, name, size, timestamps, and, when available, a lowercase hexadecimal SHA-256 `checksum`. The checksum is calculated from the exact archive bytes uploaded to Kernel. It is not a normalized checksum of the unpacked extension: archive metadata, file ordering, or compression can produce a different checksum for otherwise identical files. The value can be absent for legacy extensions and Chrome Web Store extensions that Kernel repackaged. ## Using extensions in a browser Passing the extension name or ID to the `create` method will load it into the browser. Loading an extension into a browser triggers a Chromium restart, which can take several seconds. Use [browser pools](/docs/browsers/pools) to access browsers with extensions faster. ```typescript Typescript/Javascript theme={null} import { Kernel } from '@onkernel/sdk'; const kernel = new Kernel(); const kernelBrowser = await kernel.browsers.create({ extensions: [{ name: "my-extension" }], }); ``` ```python Python theme={null} from kernel import Kernel kernel = Kernel() kernel_browser = kernel.browsers.create(extensions=[{"name": "my-extension"}]) ``` ```go Go theme={null} package main import ( "context" "github.com/kernel/kernel-go-sdk" "github.com/kernel/kernel-go-sdk/shared" ) func main() { ctx := context.Background() client := kernel.NewClient() kernelBrowser, err := client.Browsers.New(ctx, kernel.BrowserNewParams{ Extensions: []shared.BrowserExtensionParam{ {Name: kernel.String("my-extension")}, }, }) if err != nil { panic(err) } _ = kernelBrowser } ``` ```bash CLI theme={null} kernel browsers create --extension my-extension ``` ## Using extensions directly from the Chrome Web Store Kernel's CLI offers a command for fetching and unpacking extensions directly from the Chrome Web Store. Simply pass the URL of the extension you want to download and the CLI will download the extension and unpack it into the specified directory. ```bash CLI theme={null} kernel extensions download-web-store https://chromewebstore.google.com/detail/shutterfly-address-book-e/lddlpciejomhjehckimopnomegilaocb --to ./downloaded-extension ``` From here you can upload the extension to Kernel as normal. ```bash CLI theme={null} kernel extensions upload ./downloaded-extension --name my-extension ``` ## Loading an extension into a running browser If you have a browser running and would like to load an extension into it after the browser session has started, Kernel also allows you to do that via the CLI (or [API](https://kernel.sh/docs/api-reference/browsers/ad-hoc-upload-one-or-more-unpacked-extensions-to-a-running-browser-instance)): ```bash CLI theme={null} kernel browsers extensions upload ./my-extension ``` Loading an extension triggers a Chromium restart, which takes several seconds and may disrupt active CDP connections. ## Extensions requiring enterprise policies For a complete list of available extension settings and policies, refer to the [Chrome Enterprise Policy documentation](https://chromeenterprise.google/policies/extension-settings/). ### Uploading extensions requiring enterprise policies Some Chrome extensions require elevated permissions that Chrome will only grant when the extension is installed via enterprise policies. These extensions cannot be loaded with the standard `--load-extension` flag and require special handling. ### What are enterprise policy extensions? Extensions that require enterprise policies typically: * Use permissions like `webRequestBlocking` or `webRequest` with blocking capabilities * Need to intercept and modify network requests before they're sent * Require installation via Chrome's `ExtensionInstallForcelist` policy Common examples include extensions for network filtering, request signing, or advanced content modification. ### Required files for upload When uploading an extension that requires enterprise policies to Kernel, your extension directory or zip file must include: 1. **Extension source files** - Your `manifest.json` and all extension code (background scripts, content scripts, etc.) 2. **`update.xml`** - A Chrome update manifest that points to the `.crx` file location 3. **`.crx` file** - The signed and packed extension file The `.crx` file and `update.xml` are required for Kernel to serve the extension via Chrome's `ExtensionInstallForcelist` policy. If you're deploying extensions from the Chrome Web Store via `ExtensionInstallForcelist`, these files are optional since Chrome uses the Web Store's default update URL. ### Automatic detection and validation Kernel automatically detects extensions that require enterprise policies by analyzing the `manifest.json` file during upload. No manual configuration is needed. **Detection process:** 1. You upload an extension via CLI or API 2. Kernel scans the `manifest.json` for permissions like `webRequestBlocking` 3. If enterprise policies are required, Kernel validates the required files are present ### How it works Once you successfully upload an enterprise policy extension, Kernel handles the rest automatically: 1. **Upload** - You upload your extension with all required files 2. **Detection** - Kernel detects the enterprise policy requirement from the manifest 3. **Policy configuration** - Extension is automatically added to `ExtensionInstallForcelist` 4. **File serving** - The kernel-images server serves update files at `http://127.0.0.1:10001/extensions/{extension-id}/update.xml` 5. **Installation** - Chrome installs the extension via enterprise policy when the browser starts No additional HTTP server or manual policy configuration is needed. The extension works seamlessly in any browser session that it's uploaded to. # FAQ Source: https://kernel.sh/docs/browsers/faq Frequently asked questions about Kernel browsers ## Browser spin-up time Non-standard configuration can affect browser spin-up time. Settings like non-default [viewport sizing](/docs/browsers/viewport), [extensions](/docs/browsers/extensions), or [Profiles](/docs/auth/profiles) may increase the time it takes for a browser to become ready. **Standard configuration** includes: * Headful and headless browsers * Stealth mode both enabled and disabled If you're experiencing slower-than-expected browser creation times, review your configuration to identify any non-standard settings that may be contributing to the delay. ## Connection notes * **CDP connections** are meant to be long-lived but may eventually close. Websocket connections typically can remain active for up to 1 hour, after which they may close automatically. Browser sessions themselves are unaffected—reconnect to the same `cdp_ws_url` to continue using the browser. * Browsers persist independently of CDP. Depending on your timeout configuration, it will continue running even if the CDP connection closes. You can reconnect to the same `cdp_ws_url` if you're unexpectedly disconnected. * We recommend implementing reconnect logic, as network interruptions or lifecycle events can cause CDP sessions to close. Detect disconnects and automatically re-establish a CDP connection when this occurs. ## Bot detection varies by site Websites differ widely in how aggressively they detect and challenge automation, and the same site can behave differently depending on how you approach it. Rather than a fixed list of "supported" and "unsupported" sites, it's more useful to understand what drives that friction and how to reduce it. What tends to increase bot-detection friction: * **High-volume or high-concurrency scraping** — many requests from the same exit IP raise the block rate. Spread load across [proxies](/docs/proxies/overview) and reuse [Profiles](/docs/auth/profiles). * **Aggressive detection vendors** (Cloudflare, DataDome, PerimeterX, Imperva, Akamai) — these can challenge even anonymous page loads. Enable [stealth mode](/docs/browsers/bot-detection/stealth) and consider [computer controls](/docs/browsers/computer-controls) for more human-like interaction. For workflows behind a login, [Managed Auth](/docs/auth/overview) can keep sessions authenticated across runs for supported login flows. Because behavior is site- and configuration-specific, test your target site manually before automating — see the [bot detection guide](/docs/browsers/bot-detection/overview) for the recommended approach and mitigations. # File I/O Source: https://kernel.sh/docs/browsers/file-io Downloads, uploads, and manipulating the browser's filesystem ## 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. Files can only be retrieved while the browser session is still active. Once the browser session is destroyed or times out, all files from that session are permanently deleted and no longer accessible. ### Playwright Playwright performs downloads via the browser itself, so there are a few steps: * Create a browser session * Configure browser download behavior using CDP * Perform the download * Retrieve the file from the browser's filesystem With `behavior: 'default'`, user-initiated downloads (e.g., clicking a download link) are saved to the browser's default download directory. The CDP `downloadProgress` event includes a `filePath` field when the download completes, indicating exactly where the file was saved. This path can then be used with Kernel's File I/O APIs to retrieve the file. If the download is **programmatically initiated** (e.g., triggered via JavaScript rather than a user click), `behavior` must be set to `"allow"` with a specified `downloadPath`. Downloads triggered programmatically are blocked by default in Chromium, so `"default"` won't work. ```javascript theme={null} await client.send("Page.setDownloadBehavior", { behavior: "allow", downloadPath: "/tmp/downloads", }); ``` ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; import { chromium } from 'playwright'; import fs from 'fs'; import path from 'path'; import pTimeout from 'p-timeout'; const kernel = new Kernel(); // Poll listFiles until the expected file appears in the directory async function waitForFile( sessionId: 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 }); if (files.some((f) => f.name === filename)) { return; } await new Promise((r) => setTimeout(r, 500)); } throw new Error(`File ${filePath} not found after ${timeoutMs}ms`); } async function main() { const kernelBrowser = await kernel.browsers.create(); console.log('live view:', kernelBrowser.browser_live_view_url); 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()); const client = await context.newCDPSession(page); await client.send('Browser.setDownloadBehavior', { behavior: 'default', eventsEnabled: true, }); // 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) => { downloadCompletedResolve = resolve; }); client.on('Browser.downloadWillBegin', (event) => { 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(); } }); console.log('Navigating to download test page'); await page.goto('https://browser-tests-alpha.vercel.app/api/download-test'); await page.getByRole('link', { name: 'Download File' }).click(); try { await pTimeout(downloadCompleted, { milliseconds: 10_000, message: new Error('Download timed out after 10 seconds'), }); console.log('Download completed'); } catch (err) { console.error(err); throw err; } if (downloadState === 'canceled') { throw new Error('Download was canceled'); } if (!downloadFilePath) { throw new Error('Unable to determine download file path'); } // Wait for the file to be available via Kernel's File I/O APIs console.log(`Waiting for file: ${downloadFilePath}`); await waitForFile(kernelBrowser.session_id, downloadFilePath); console.log(`Reading file: ${downloadFilePath}`); const resp = await kernel.browsers.fs.readFile(kernelBrowser.session_id, { path: downloadFilePath, }); const bytes = await resp.bytes(); fs.mkdirSync('downloads', { recursive: true }); const localPath = `downloads/${path.basename(downloadFilePath)}`; fs.writeFileSync(localPath, bytes); console.log(`Saved to ${localPath}`); await kernel.browsers.deleteByID(kernelBrowser.session_id); console.log('Kernel browser deleted successfully.'); } main(); ``` ```python Python theme={null} import asyncio import os from pathlib import Path import time from kernel import Kernel from playwright.async_api import async_playwright kernel = Kernel() # Poll list_files until the expected file appears in the directory async def wait_for_file( 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_path) if any(f.name == filename for f in files): return await asyncio.sleep(0.5) raise TimeoutError(f"File {file_path} not found after {timeout_sec}s") async def main(): kernel_browser = kernel.browsers.create() print("Kernel browser live view url:", kernel_browser.browser_live_view_url) async with async_playwright() as playwright: browser = await playwright.chromium.connect_over_cdp(kernel_browser.cdp_ws_url) context = browser.contexts[0] page = context.pages[0] if len(context.pages) > 0 else await context.new_page() cdp_session = await context.new_cdp_session(page) await cdp_session.send( "Browser.setDownloadBehavior", { "behavior": "default", "eventsEnabled": True, }, ) download_completed = asyncio.Event() download_file_path: str | None = None download_state: str | None = None def _on_download_begin(event): print(f"Download started: {event.get('suggestedFilename', 'unknown')}") def _on_download_progress(event): 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) cdp_session.on("Browser.downloadProgress", _on_download_progress) print("Navigating to download test page") await page.goto("https://browser-tests-alpha.vercel.app/api/download-test") await page.get_by_role("link", name="Download File").click() try: await asyncio.wait_for(download_completed.wait(), timeout=10) print("Download completed") except asyncio.TimeoutError: print("Download timed out after 10 seconds") raise if download_state == "canceled": raise RuntimeError("Download was canceled") if not download_file_path: raise RuntimeError("Unable to determine download file path") # Wait for the file to be available via Kernel's File I/O APIs 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=download_file_path ) 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}") kernel.browsers.delete_by_id(kernel_browser.session_id) print("Kernel browser deleted successfully.") if __name__ == "__main__": asyncio.run(main()) ``` ### Stagehand When using Stagehand with Kernel browsers, downloads are saved to the browser's filesystem and retrieved with Kernel's File I/O APIs. The setup differs by version (see the [Stagehand integration guide](/docs/integrations/stagehand) for the full connection setup): * **v4** connects to the running browser, and a user-initiated download — e.g. clicking a download link — is saved to the browser's default download directory, `/home/kernel/Downloads`. No download-specific configuration is required. * **v3** sets the download directory via `localBrowserLaunchOptions` (`downloadsPath` + `acceptDownloads`). Here's a complete example for each version: ```typescript Stagehand v4 theme={null} import { Stagehand, localBrowser } from "@browserbasehq/stagehand"; import Kernel from "@onkernel/sdk"; import fs from "fs"; import { createReadStream } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; // Kernel browsers save user-initiated downloads here by default. const DOWNLOAD_DIR = "/home/kernel/Downloads"; // Mirror the Stagehand extension onto the running browser so localBrowser.connect can load it. const stagehandDist = dirname(fileURLToPath(import.meta.resolve("@browserbasehq/stagehand"))); async function loadStagehandExtension(kernel: Kernel, sessionId: string) { await kernel.browsers.fs.uploadZip(sessionId, { dest_path: join(stagehandDist, "extension"), zip_file: createReadStream(join(stagehandDist, "assets/stagehand-extension.zip")), }); } // Poll listFiles until a completed file appears (skip in-progress .crdownload files). 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 }); const done = files.find((f) => !f.name.endsWith(".crdownload")); if (done) { return done; } 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}`); let stagehand: Awaited> | undefined; let browser: Awaited> | undefined; try { await loadStagehandExtension(kernel, kernelBrowser.session_id); browser = await localBrowser.connect({ cdpUrl: kernelBrowser.cdp_ws_url }); stagehand = await Stagehand.create({ browser, model: { modelName: "anthropic/claude-sonnet-4-5", apiKey: process.env.MODEL_API_KEY, }, }); const page = await browser.context.activePage(); if (!page) throw new Error("No active page in the Kernel browser"); 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 the 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}`); } finally { // Nested so a rejected close() never skips deleting the Kernel browser. try { await stagehand?.close(); } finally { try { await browser?.close(); } finally { await kernel.browsers.deleteByID(kernelBrowser.session_id); } } } console.log("Browser session closed"); } main().catch((err) => { console.error(err); process.exit(1); }); ``` ```typescript Stagehand v3 theme={null} 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, // Specify where downloads should be saved acceptDownloads: true, // Enable downloads }, }); 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. ## Uploads 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 theme={null} import Kernel from '@onkernel/sdk'; import { chromium } from 'playwright'; const IMAGE_URL = 'https://www.kernel.sh/brand_assets/Kernel-Logo_Accent.png'; const kernel = new Kernel(); async function main() { // Create Kernel browser session const kernelBrowser = await kernel.browsers.create(); console.log('Live view:', kernelBrowser.browser_live_view_url); // 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 a page with a file input await page.goto('https://browser-tests-alpha.vercel.app/api/upload-test'); // Fetch file and pass buffer directly to setInputFiles const response = await fetch(IMAGE_URL); const buffer = Buffer.from(await response.arrayBuffer()); 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'); } main(); ``` ```python Python theme={null} import asyncio 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() async def main(): # Create Kernel browser session kernel_browser = kernel.browsers.create() print(f'Live view: {kernel_browser.browser_live_view_url}') 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 a page with a file input await page.goto('https://browser-tests-alpha.vercel.app/api/upload-test') # 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() kernel.browsers.delete_by_id(kernel_browser.session_id) print('Browser deleted') if __name__ == '__main__': asyncio.run(main()) ``` ## Considerations * 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. # GPU Acceleration Source: https://kernel.sh/docs/browsers/gpu-acceleration GPU acceleration enables GPU-accelerated rendering in Kernel browsers, providing enhanced performance for graphics-intensive workloads. GPU acceleration is only available for headful browsers and does not support [standby mode](/docs/browsers/standby). GPU browsers have [separate pricing](https://kernel.sh/pricing) from headless or headful browsers. ## Enable GPU acceleration Set the `gpu` parameter to `true` when creating a browser: ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); const kernelBrowser = await kernel.browsers.create({ gpu: true }); ``` ```python Python theme={null} from kernel import Kernel kernel = Kernel() kernel_browser = kernel.browsers.create( gpu=True ) ``` ```go Go theme={null} package main import ( "context" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() kernelBrowser, err := client.Browsers.New(ctx, kernel.BrowserNewParams{ GPU: kernel.Bool(true), }) if err != nil { panic(err) } _ = kernelBrowser } ``` You can also enable GPU acceleration in the dashboard when deploying a browser under **Advanced Configuration**. ## Use cases GPU acceleration is useful for: * High performance live view streaming * Rendering complex 3D graphics or WebGL content * Video processing and playback * Canvas-heavy applications GPU acceleration speeds up page rendering, not screenshot capture. CDP's `Page.captureScreenshot` latency is unaffected by GPU. [Computer controls screenshots](/docs/browsers/computer-controls#take-screenshots) see a small speedup with GPU enabled. ## Availability GPU acceleration is available on [Start-Up and Enterprise plans](https://kernel.sh/pricing). # Headless Mode Source: https://kernel.sh/docs/browsers/headless Kernel browsers ship in `headful` mode by default. In headful mode, the launched browser has a corresponding GUI. This enables features like [live view](/docs/browsers/live-view) and [replays](/docs/browsers/replays). `headless` mode runs without a visual interface. They generally run faster and have a lighter footprint (1 GB rather than headful's 8 GB), resulting in significant cost savings. This is useful for short-lived or highly concurrent browser automations. Some bot detectors may detect headless mode. To launch a Kernel browser in `headless` mode, set its config: ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); const kernelBrowser = await kernel.browsers.create({ headless: true, }); ``` ```python Python theme={null} from kernel import Kernel kernel = Kernel() kernel_browser = kernel.browsers.create(headless=True) ``` ```go Go theme={null} package main import ( "context" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() kernelBrowser, err := client.Browsers.New(ctx, kernel.BrowserNewParams{ Headless: kernel.Bool(true), }) if err != nil { panic(err) } _ = kernelBrowser } ``` [Live View](/docs/browsers/live-view) and [Replays](/docs/browsers/replays) are not available in headless mode. # Live View Source: https://kernel.sh/docs/browsers/live-view Humans-in-the-loop can access the live view of Kernel browsers in real-time to resolve errors or take unscripted actions. To access the live view, visit the `browser_live_view_url` provided when you create a Kernel browser: ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); const browser = await kernel.browsers.create(); console.log(browser.browser_live_view_url); ``` ```python Python theme={null} from kernel import Kernel kernel = Kernel() browser = kernel.browsers.create() print(browser.browser_live_view_url) ``` ```go Go theme={null} package main import ( "context" "fmt" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() browser, err := client.Browsers.New(ctx, kernel.BrowserNewParams{}) if err != nil { panic(err) } fmt.Println(browser.BrowserLiveViewURL) } ``` ## Query parameters The `browser_live_view_url` supports additional query parameters to customize the live view: * `readOnly` (bool): when set to `true`, the view will be non-interactive. Example: ``` https://api.onkernel.com/browser/live/?readOnly=true ``` ## Embedding in an iframe The live view URL can be embedded in an iframe to integrate the browser view into your own application or dashboard. ```html theme={null} ``` Embedded third-party iframes like live view must have focus to receive keyboard events. On Safari, focus requires a user-initiated event — calling `.focus()` on the iframe element within a user-initiated event handler is recommended. To enable clipboard sharing, add `allow="autoplay; clipboard-read; clipboard-write"` to the iframe element. Kernel serves live views from `*.onkernel.com:8443` and `*.kernel.sh:8443`. If your application uses a **Content Security Policy (CSP)**, you must add the following directives to allow the live view iframe and its WebSocket connection: ``` frame-src https://*.onkernel.com:8443 https://*.kernel.sh:8443; connect-src https://*.onkernel.com:8443 wss://*.onkernel.com:8443 https://*.kernel.sh:8443 wss://*.kernel.sh:8443; ``` ## Kiosk mode Kiosk mode provides a fullscreen live view experience without browser UI elements like the address bar and tabs. You can enable kiosk mode when creating a browser by setting the `kiosk_mode` parameter to `true`. Kiosk mode triggers a Chromium restart, which can take several seconds. Use [browser pools](/docs/browsers/pools) to access kiosk mode browsers faster. ```typescript Typescript/Javascript theme={null} const browser = await kernel.browsers.create({ kiosk_mode: true }); ``` ```python Python theme={null} kernel_browser = kernel.browsers.create( kiosk_mode=True ) ``` ```go Go theme={null} browser, err := client.Browsers.New(ctx, kernel.BrowserNewParams{ KioskMode: kernel.Bool(true), }) if err != nil { panic(err) } _ = browser ``` ## URL lifetime `browser_live_view_url` becomes invalid once the browser is [deleted](/docs/browsers/termination) manually or via timeout. # Performance Source: https://kernel.sh/docs/browsers/performance Kernel browsers [benchmark](https://www.kernel.sh/benchmarks) as the fastest, most performant browsers in the market. ### Kernel browser creation latency | Benchmark | Latency | Last reported | | --------- | ------- | -------------- | | P50 | 30ms | April 24, 2026 | | P99 | 105ms | April 24, 2026 | ### Troubleshooting latency If you're experiencing slower-than-expected browser creation (or [browser pool acquisition](/docs/browsers/pools)), review your configuration for the following: 1. App code ⇔ Kernel browser region Kernel browsers run in `us-east`. Use our [app platform](/docs/apps/develop) to colocate your browser agent or automation. 2. Create browser rate limit Kernel enforces [rate limits](/docs/info/pricing#rate-limiting) on browser creation based on your plan. Our SDKs automatically retry, respecting the `Retry-After` header for delay timing. If retries are exhausted, the SDK throws a typed `RateLimitError` with the response headers accessible for custom backoff logic. 3. Non-default browser configurations Certain browser configurations trigger Chromium to restart, which can take several seconds. Use [browser pools](/docs/browsers/pools) to access browsers with custom configurations faster. The following configurations cause browser restarts, as well as disrupt active CDP connections: * Custom viewport configurations * Chrome extensions * Setting the live view to `kiosk mode` 4. Browser pool refill rate Browser pools fill at a [specified rate](https://www.kernel.sh/docs/api-reference/browser-pools/create-a-browser-pool#body-fill-rate-per-minute). Read about browser pool lifecycle best practices [here](/docs/browsers/pools#how-browser-pools-work). # Using Playwright with Computer Use Fallback Source: https://kernel.sh/docs/browsers/playwright-computer-use-fallback Run an agent with fast, DOM-based Playwright tools, then fall back to computer use only when the DOM won't cooperate [Playwright](https://github.com/microsoft/playwright) is Microsoft's open-source browser automation framework. It controls Chromium, Firefox, and WebKit through one API, with resilient element locators, automatic waiting, isolated browser contexts, and detailed tracing when something goes wrong. Higher-level browser-agent frameworks often wrap Playwright in model-friendly abstractions. Those frameworks can be useful, but Playwright remains a strong starting point: it is widely adopted, well documented, and gives developers direct, inspectable control over the browser. For agents, DOM-based Playwright actions are usually faster, cheaper, and more predictable than reasoning from screenshots and clicking screen coordinates. Some page interactions still cannot be completed reliably through DOM-based tools. Pointer-driven drag-and-drop is a common example. Libraries such as `dnd-kit` and `SortableJS` may require several intermediate `mousemove` events before they recognize a drag. A tool that jumps directly from pressing to releasing can report success even though the page never recognized the gesture and the card did not move. The answer is not to run the entire task through slower, screenshot-based computer use. Start with Playwright for navigation, reading, clicking, and form filling. Switch to computer use only for the interaction that needs realistic pointer movement, then continue without losing the browser's open tabs, page state, or the agent's conversation context. This cookbook demonstrates three ways to make that handoff between Playwright and computer use with [`@onkernel/browser-loop`](https://www.npmjs.com/package/@onkernel/browser-loop), KERNEL's tool package for browser agents. ## The example task Every snippet below runs the same task against [magnitasks.com](https://magnitasks.com), a public Kanban-style board: ``` Go to magnitasks.com, navigate to Tasks, filter for Alice's tasks, then drag every item card to the Done column. ``` Filtering and navigating work fine through the DOM. Dragging a card onto the board doesn't — `magnitasks.com`'s board uses pointer-based drag-and-drop, so it's a small, honest example of the failure mode this cookbook exists for. ## Playwright vs. computer use An agent tool is a callable operation that lets the model read browser state or take an action. The model selects a tool and supplies its inputs; the harness executes the operation and returns the result to the model. A **toolset** is the collection of tools available during a run. `browser-loop` provides two toolsets for controlling the same browser session. **Playwright tools** (`loop.toolsets.browser()`) find page elements through the DOM and act on them by reference. **Computer-use tools** (`loop.toolsets.computer()`) read screenshots and control the pointer using screen coordinates. | | Playwright | computer use | | ---------- | -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | | tools | `browser_*` | `computer_*` | | driven by | an accessibility snapshot, resolved by ref | a screenshot, read pixel by pixel | | a click is | one DOM lookup + one CDP dispatch | a vision call, then an OS-level click at a coordinate — see [Computer Controls](/docs/browsers/computer-controls) | | cost | cheap, fast, deterministic when the DOM cooperates | a full model round trip per action, but works on anything the page renders regardless of DOM structure | Make Playwright your default toolset: it's faster and cheaper per action. Reach for [computer use](/docs/integrations/computer-use/overview) for the specific interactions that don't hold up to DOM-ref execution: drag-and-drop on a pointer-sensor library, canvas-drawn UI, a file picker's native dialog, anything a screenshot can see that the accessibility tree can't reliably resolve. ## Picking a harness The snippets below use `browser-loop` with [`@earendil-works/pi-agent-core`](https://www.npmjs.com/package/@earendil-works/pi-agent-core)'s `AgentHarness`. `compiled.apply(harness)` swaps a *running* harness onto a new (model, tools) pair without resetting browser refs, open tabs, or the conversation transcript, which is what makes the mid-session handoff between Playwright and computer-use tools a single method call. The model that picks up with computer-use tools still has the entire Playwright conversation as context, and knows what it already tried. If you use a different agent harness, the same concepts apply. If your setup doesn't expose an equivalent live tool-catalog swap, implement the handoff as two sequential calls instead: run Playwright to completion or failure, then start a fresh call with computer-use tools and carry forward what happened as plain-text context in the new prompt. The Per-Tool Limit example below shows what to include in that handoff message. ## Setup ```bash theme={null} npm install @onkernel/browser-loop @onkernel/sdk @earendil-works/pi-agent-core tsx ``` Every script needs a `KERNEL_API_KEY` and a provider key for whichever model `LOOP_MODEL` points at (`anthropic:claude-sonnet-5` by default, so `ANTHROPIC_API_KEY`): ```bash theme={null} KERNEL_API_KEY=... ANTHROPIC_API_KEY=... npx tsx playwright-computer-use-per-tool-limit.ts KERNEL_API_KEY=... ANTHROPIC_API_KEY=... npx tsx playwright-computer-use-total-tool-call-limit.ts KERNEL_API_KEY=... ANTHROPIC_API_KEY=... npx tsx playwright-computer-use-model-directed-handoff.ts ``` ## Three ways to hand off between Playwright and computer use All three run Playwright first and switch to computer use once it stops making progress. They differ in what you need to know about the task at integration time: | what you know | approach | integration cost | | ------------------------------------------------------------------ | ------------------------------------------------- | ------------------------------------------------------------------ | | the specific Playwright tool likely to be unreliable for this task | [Per-Tool Limit](#per-tool-limit) | lowest — one config line, no tuning | | the general shape of the task, not the specific risky action | [Total Tool-Call Limit](#total-tool-call-limit) | needs a per-task number, found by testing | | little to nothing — tasks arrive from an end user at runtime | [Model-Directed Handoff](#model-directed-handoff) | no handoff logic, but no bound on the model's own switching either | If you can name the risky tool, use **Per-Tool Limit** — it's the only one of the three that's both precise and free of tuning. The other two exist for when the task genuinely isn't known until runtime. ### Per-Tool Limit Use this when you're integrating against a known, fixed target and you already know — or can find in one test run — which specific Playwright tool needs help from computer-use tools. `PLAYWRIGHT_TOOLS_WITH_ATTEMPT_LIMITS` names those tools; the harness counts completed attempts for each one and hands off when any tool reaches `PER_TOOL_ATTEMPT_LIMIT`. Most DOM-ref tools report success even when they don't produce the intended effect, so an attempt against a still-unfinished task is itself the signal. `ACTION_SAFETY_CAP` is a backstop for a run that never reaches a per-tool limit. ```ts theme={null} /** * Per-Tool Limit: Playwright first, fall back to computer-use tools after a * task-critical Playwright tool reaches its per-tool attempt limit without * finishing the job. * * `browser_drag` (the atomic tool @onkernel/browser-loop compiles for * the Playwright toolset) drags by resolving two DOM/viewport points and * dispatching exactly one CDP `mousePressed` -> `mouseMoved` -> `mouseReleased` * sequence between them. Kanban boards built on pointer-based drag-and-drop * (dnd-kit, SortableJS, most React DnD setups) need several intermediate * `mousemove` events to cross their drag-activation threshold, so a single * jump often never registers as a drag at all -- the card just gets a click. * The tool call itself still reports success: there's no error to catch, only * a card that never moved. * * `computer_drag` (from the computer-use toolset) takes a multi-point pixel * path and dispatches a real mousemove sequence along it, which is enough * motion for those same libraries to pick up the gesture. * * This script hands off to computer-use tools once any tool in * PLAYWRIGHT_TOOLS_WITH_ATTEMPT_LIMITS reaches PER_TOOL_ATTEMPT_LIMIT and the * task still isn't done. Each selected tool has its own attempt count, so * setup steps like navigation and filtering never consume another tool's * limit. `browser_drag` is selected here because drag-and-drop is this task's * known-unreliable action; adapt the set to the Playwright tools your task * expects to need computer-use help for. An action safety cap is a backstop * for pages that never reach a per-tool limit. * * Demo target: magnitasks.com, a Kanban-style task board (Tasks page, drag * cards between columns, filter by assignee). Its board uses pointer-based * drag-and-drop, so `browser_drag` reliably fails to move cards while * `computer_drag` succeeds -- a good, honest example of the failure mode this * fallback exists for. * * KERNEL's replay API records the failed Playwright drags and the computer-use * recovery on one video. The replay view URL prints as soon as recording * starts and again once it's stopped and finished processing. * * Usage: * KERNEL_API_KEY=... ANTHROPIC_API_KEY=... npx tsx playwright-computer-use-per-tool-limit.ts * * Env: * KERNEL_API_KEY required, KERNEL browser API key * LOOP_MODEL optional, defaults to anthropic:claude-sonnet-5 * (needs the matching provider API key, e.g. ANTHROPIC_API_KEY) */ import KERNEL from "@onkernel/sdk"; import { AgentHarness, InMemorySessionRepo, type AgentHarnessEvent } from "@earendil-works/pi-agent-core"; import { loop } from "@onkernel/browser-loop"; import { attach, requireLoopEnvApiKeyForModel, type LoopModelRef } from "@onkernel/browser-loop/pi"; const TASK_PROMPT = "Go to magnitasks.com, navigate to Tasks, filter for Alice's tasks, " + "then drag every item card to the Done column."; const MODEL = (process.env.LOOP_MODEL as LoopModelRef | undefined) ?? "anthropic:claude-sonnet-5"; // Playwright tools this task depends on and that are known to be unreliable. // Each selected tool gets its own attempt count. These tools report no // execution error even when they don't produce the intended effect, so an // attempt that doesn't finish the task is itself the signal. const PLAYWRIGHT_TOOLS_WITH_ATTEMPT_LIMITS = new Set(["browser_drag"]); const PER_TOOL_ATTEMPT_LIMIT = 2; // Backstop: total tool calls Playwright gets regardless of whether it // ever reaches a drag, so a page that never triggers one (wrong selector, // navigation failure) can't run forever. const ACTION_SAFETY_CAP = 20; async function main(): Promise { const kernelApiKey = process.env.KERNEL_API_KEY; if (!kernelApiKey) throw new Error("KERNEL_API_KEY is required"); requireLoopEnvApiKeyForModel(MODEL); const client = new KERNEL({ apiKey: kernelApiKey }); const browser = await client.browsers.create({ stealth: true }); const replay = await client.browsers.replays.start(browser.session_id); console.log(`recording started: ${replay.replay_view_url}`); const kb = attach({ client, browser }); try { const session = await new InMemorySessionRepo().create({ id: "playwright-computer-use-per-tool-limit" }); const playwrightPair = kb.compile({ model: MODEL, tools: loop.toolsets.browser(), }); const harness = new AgentHarness({ session, model: playwrightPair.model, models: playwrightPair.models, tools: [...playwrightPair.tools], activeToolNames: playwrightPair.tools.map((tool) => tool.name), systemPrompt: "Use the supplied browser tools to complete the task efficiently.", }); playwrightPair.activate(harness); let actionTurns = 0; const attemptsByTool = new Map(); let handoffRequested = false; const unsubscribe = harness.subscribe((event: AgentHarnessEvent) => { if (event.type !== "tool_execution_end") return; actionTurns += 1; let attemptStatus = ""; let reachedAttemptLimit = false; if (PLAYWRIGHT_TOOLS_WITH_ATTEMPT_LIMITS.has(event.toolName)) { const attempts = (attemptsByTool.get(event.toolName) ?? 0) + 1; attemptsByTool.set(event.toolName, attempts); attemptStatus = ` (attempt ${attempts}/${PER_TOOL_ATTEMPT_LIMIT})`; reachedAttemptLimit = attempts >= PER_TOOL_ATTEMPT_LIMIT; } console.log(`[playwright ${actionTurns}] ${event.toolName} error=${event.isError}${attemptStatus}`); if (handoffRequested) return; if (reachedAttemptLimit) { handoffRequested = true; console.log("[playwright] per-tool attempt limit reached, aborting run to switch to computer-use tools"); void harness.abort(); } else if (actionTurns >= ACTION_SAFETY_CAP) { handoffRequested = true; console.log("[playwright] action safety cap reached, aborting run to switch to computer-use tools"); void harness.abort(); } }); console.log(`model=${MODEL} toolset=playwright prompt=${JSON.stringify(TASK_PROMPT)}`); let final = await harness.prompt(TASK_PROMPT); unsubscribe(); if (final.stopReason === "aborted") { console.log("[computer-use] Playwright did not finish within its limits, switching toolsets"); const computerPair = kb.compile({ model: MODEL, tools: loop.toolsets.computer(), }); await computerPair.apply(harness); console.log(`model=${MODEL} toolset=computer`); final = await harness.prompt( "Playwright drag actions were not registering on this page's drag-and-drop board -- " + "the cards weren't moving. Take a screenshot to see where things stand, then finish the " + "task using the computer-use tools: click and drag by pixel coordinates instead of DOM " + "refs. For computer_drag, use a path with several intermediate waypoints between the start " + "and end so the board can see the pointer move and recognize a drag.", ); } console.log(`final stopReason: ${final.stopReason}`); for (const block of final.content) { if (block.type === "text") console.log(block.text); } } finally { await kb.dispose(); await client.browsers.replays.stop(replay.replay_id, { id: browser.session_id }); console.log(`replay ready: ${replay.replay_view_url}`); await client.browsers.deleteByID(browser.session_id); } } void main(); ``` ### Total Tool-Call Limit Use this when you know the general category of task but not the specific action likely to need computer use, so naming one tool up front isn't realistic. `TOTAL_TOOL_CALL_LIMIT` counts every completed Playwright tool call and hands off when the total reaches that limit, regardless of which tools the agent used. Set the limit high enough to cover the task's legitimate setup — navigation, filtering, form-filling — before it reaches the action that needs computer use. There's no principled way to pick the number without running the task and looking; that's the direct cost of not needing to know which tool will fail. ```ts theme={null} /** * Total Tool-Call Limit: Playwright first, fall back to computer-use tools after * a total tool-call limit. Use this when you know the general shape of the * task without knowing which specific Playwright tool will be unreliable. Use * this over the per-tool attempt limit when you can't name the risky action in * advance; use it over model-directed handoff when you want a deterministic, * bounded Playwright run instead of trusting a system prompt to self-limit. * * The limit counts every completed tool call, so it has to be generous * enough to survive whatever setup (navigation, filtering, form-filling) the * task legitimately needs before it reaches the action that actually needs * computer use. Too tight and it stops during setup before ever attempting * the risky action. Picking the right number takes running the task and * looking; that imprecision is the tradeoff for skipping the step of naming * which tool will fail. * * Demo target: magnitasks.com, a Kanban-style task board (Tasks page, drag * cards between columns, filter by assignee). See * playwright-computer-use-per-tool-limit.ts for why browser_drag * reliably fails here and computer_drag reliably succeeds. * * KERNEL's replay API records both toolsets on one video. * * Usage: * KERNEL_API_KEY=... ANTHROPIC_API_KEY=... npx tsx playwright-computer-use-total-tool-call-limit.ts * * Env: * KERNEL_API_KEY required, KERNEL browser API key * LOOP_MODEL optional, defaults to anthropic:claude-sonnet-5 * (needs the matching provider API key, e.g. ANTHROPIC_API_KEY) */ import KERNEL from "@onkernel/sdk"; import { AgentHarness, InMemorySessionRepo, type AgentHarnessEvent } from "@earendil-works/pi-agent-core"; import { loop } from "@onkernel/browser-loop"; import { attach, requireLoopEnvApiKeyForModel, type LoopModelRef } from "@onkernel/browser-loop/pi"; const TASK_PROMPT = "Go to magnitasks.com, navigate to Tasks, filter for Alice's tasks, " + "then drag every item card to the Done column."; const MODEL = (process.env.LOOP_MODEL as LoopModelRef | undefined) ?? "anthropic:claude-sonnet-5"; // Total Playwright tool calls allowed before the handoff to computer-use tools. // Set this high enough to cover normal setup plus a few attempts at the action // that needs computer use. const TOTAL_TOOL_CALL_LIMIT = 10; async function main(): Promise { const kernelApiKey = process.env.KERNEL_API_KEY; if (!kernelApiKey) throw new Error("KERNEL_API_KEY is required"); requireLoopEnvApiKeyForModel(MODEL); const client = new KERNEL({ apiKey: kernelApiKey }); const browser = await client.browsers.create({ stealth: true }); const replay = await client.browsers.replays.start(browser.session_id); console.log(`recording started: ${replay.replay_view_url}`); const kb = attach({ client, browser }); try { const session = await new InMemorySessionRepo().create({ id: "playwright-computer-use-total-tool-call-limit" }); const playwrightPair = kb.compile({ model: MODEL, tools: loop.toolsets.browser(), }); const harness = new AgentHarness({ session, model: playwrightPair.model, models: playwrightPair.models, tools: [...playwrightPair.tools], activeToolNames: playwrightPair.tools.map((tool) => tool.name), systemPrompt: "Use the supplied browser tools to complete the task efficiently.", }); playwrightPair.activate(harness); let toolCalls = 0; let handoffRequested = false; const unsubscribe = harness.subscribe((event: AgentHarnessEvent) => { if (event.type !== "tool_execution_end") return; toolCalls += 1; console.log(`[playwright ${toolCalls}/${TOTAL_TOOL_CALL_LIMIT}] ${event.toolName} error=${event.isError}`); if (toolCalls >= TOTAL_TOOL_CALL_LIMIT && !handoffRequested) { handoffRequested = true; console.log("[playwright] total tool-call limit reached, aborting run to switch to computer-use tools"); void harness.abort(); } }); console.log(`model=${MODEL} toolset=playwright prompt=${JSON.stringify(TASK_PROMPT)}`); let final = await harness.prompt(TASK_PROMPT); unsubscribe(); if (final.stopReason === "aborted") { console.log("[computer-use] Playwright did not finish within its limit, switching toolsets"); const computerPair = kb.compile({ model: MODEL, tools: loop.toolsets.computer(), }); await computerPair.apply(harness); console.log(`model=${MODEL} toolset=computer`); final = await harness.prompt( "Playwright drag actions were not registering on this page's drag-and-drop board -- " + "the cards weren't moving. Take a screenshot to see where things stand, then finish the " + "task using the computer-use tools: click and drag by pixel coordinates instead of DOM " + "refs. For computer_drag, use a path with several intermediate waypoints between the start " + "and end so the board can see the pointer move and recognize a drag.", ); } console.log(`final stopReason: ${final.stopReason}`); for (const block of final.content) { if (block.type === "text") console.log(block.text); } } finally { await kb.dispose(); await client.browsers.replays.stop(replay.replay_id, { id: browser.session_id }); console.log(`replay ready: ${replay.replay_view_url}`); await client.browsers.deleteByID(browser.session_id); } } void main(); ``` ### Model-Directed Handoff Use this when the task isn't known at integration time at all — a general-purpose agent product where an end user's request determines the site, the workflow, and whether anything needs computer use. `loop.toolsets.mixed()` gives the model both Playwright and computer-use tools from the start; a system prompt steers it toward the cheaper `browser_*` tools by default and toward `computer_*` tools once a `browser_*` action doesn't produce the expected effect. There's no handoff code to write. The tradeoff is that a system prompt is a soft constraint on both axes that matter: how long the model sticks with Playwright tools before trying computer use, and which computer-use tool it reaches for once it does. Nothing here enforces a hard bound, and nothing guarantees it picks the purpose-built tool for the job over reassembling the effect from lower-level primitives. ```ts theme={null} /** * Model-Directed Handoff: give the model both Playwright and computer-use tools up * front (loop.toolsets.mixed()) and let it choose per action, steered by a * system prompt toward Playwright tools by default. Use model-directed handoff * when you don't want to write handoff logic for a general-purpose agent whose * tasks aren't known ahead of time, so you can't preselect a limited tool or * set a reasonable total tool-call limit. * * The tradeoff: with both toolsets available, a model can try browser_* * tools first, take a screenshot, notice a drag hasn't landed, and switch to * computer_* tools on its own -- but it may never call the purpose-built * computer_drag tool. It can reassemble a drag by hand from * computer_mouse_down / computer_move / computer_mouse_up primitives * instead, taking far more tool calls -- and therefore model round trips -- * for the same result. A system prompt is a soft constraint on both when it * switches and which tool it reaches for once it does; naming computer_drag * explicitly in a handoff message (as the scripted variants do) is what * keeps computer use fast once it's in control. Nothing here enforces that a * real handoff even happens -- there's no hard bound on how long the model * sticks with browser_* tools before trying computer_* ones. * * Demo target: magnitasks.com, a Kanban-style task board (Tasks page, drag * cards between columns, filter by assignee). See * playwright-computer-use-per-tool-limit.ts for why browser_drag * reliably fails here and computer_drag reliably succeeds. * * The session is recorded end to end with KERNEL's replay API. * * Usage: * KERNEL_API_KEY=... ANTHROPIC_API_KEY=... npx tsx playwright-computer-use-model-directed-handoff.ts * * Env: * KERNEL_API_KEY required, KERNEL browser API key * LOOP_MODEL optional, defaults to anthropic:claude-sonnet-5 * (needs the matching provider API key, e.g. ANTHROPIC_API_KEY) */ import KERNEL from "@onkernel/sdk"; import { AgentHarness, InMemorySessionRepo, type AgentHarnessEvent } from "@earendil-works/pi-agent-core"; import { loop } from "@onkernel/browser-loop"; import { attach, requireLoopEnvApiKeyForModel, type LoopModelRef } from "@onkernel/browser-loop/pi"; const TASK_PROMPT = "Go to magnitasks.com, navigate to Tasks, filter for Alice's tasks, " + "then drag every item card to the Done column."; const MODEL = (process.env.LOOP_MODEL as LoopModelRef | undefined) ?? "anthropic:claude-sonnet-5"; async function main(): Promise { const kernelApiKey = process.env.KERNEL_API_KEY; if (!kernelApiKey) throw new Error("KERNEL_API_KEY is required"); requireLoopEnvApiKeyForModel(MODEL); const client = new KERNEL({ apiKey: kernelApiKey }); const browser = await client.browsers.create({ stealth: true }); const replay = await client.browsers.replays.start(browser.session_id); console.log(`recording started: ${replay.replay_view_url}`); const kb = attach({ client, browser }); try { const session = await new InMemorySessionRepo().create({ id: "playwright-computer-use-model-directed-handoff" }); const compiled = kb.compile({ model: MODEL, tools: loop.toolsets.mixed() }); const harness = new AgentHarness({ session, model: compiled.model, models: compiled.models, tools: [...compiled.tools], activeToolNames: compiled.tools.map((tool) => tool.name), systemPrompt: "Use the browser_* tools (ref-based, DOM-driven) as your default for navigation, clicking, " + "filling forms, and reading page state -- they're faster and cheaper. Only reach for the " + "computer_* tools (screenshot-driven, pixel-coordinate based) when a browser_* action, " + "especially browser_drag, doesn't produce the effect you expected after a snapshot or " + "screenshot check. Don't retry a failing browser_* action more than once before switching " + "to the computer_* equivalent for that specific step.", }); compiled.activate(harness); let actionTurns = 0; harness.subscribe((event: AgentHarnessEvent) => { if (event.type !== "tool_execution_end") return; actionTurns += 1; console.log(`[${actionTurns}] ${event.toolName} error=${event.isError}`); }); console.log(`model=${MODEL} toolset=mixed prompt=${JSON.stringify(TASK_PROMPT)}`); const final = await harness.prompt(TASK_PROMPT); console.log(`final stopReason: ${final.stopReason}`); for (const block of final.content) { if (block.type === "text") console.log(block.text); } } finally { await kb.dispose(); await client.browsers.replays.stop(replay.replay_id, { id: browser.session_id }); console.log(`replay ready: ${replay.replay_view_url}`); await client.browsers.deleteByID(browser.session_id); } } void main(); ``` ## Choosing between them | approach | needs knowing | integration effort | switching behavior | | ---------------------- | -------------------------------------- | -------------------------------------- | ------------------------------------------------------------- | | Per-Tool Limit | the exact risky tool | one config line, no tuning | fires when a selected tool reaches its attempt limit | | Total Tool-Call Limit | enough about the task to size a number | a number to find and re-check per task | fires on total tool-call count, regardless of which tools ran | | Model-Directed Handoff | nothing in advance | no handoff logic at all | left entirely to the model's own judgment, with no hard bound | As a starting rule, reach for **Per-Tool Limit** first, even if it costs you one exploratory run to find the tool name. Use **Total Tool-Call Limit** only when the task varies enough that naming a specific tool isn't realistic. Use **Model-Directed Handoff** only when you can't write task-specific logic at all — and if you do, consider pairing it with a hard action cap as a backstop against its unbounded soft constraint. ## Notes * **None of the three hand control back.** Once a run switches to computer-use tools, Playwright doesn't get another turn in that session. Fine for a single bounded task; worth revisiting for a longer-running agent that could benefit from returning to the cheaper tools once computer-use tools re-establishes progress. * **Swap models freely.** All three default to `anthropic:claude-sonnet-5` via `LOOP_MODEL`. Use `listLoopModels()` from `@onkernel/browser-loop/pi` to see everything else the catalog supports for both toolsets. * **The whole handoff is recorded on one video.** Each script wraps the run in `client.browsers.replays.start()` / `.stop()`, so the Playwright attempt and the computer-use recovery land on the same replay. The replay URL prints as soon as recording starts and again once it's stopped and finished processing. ## Next steps * [Computer Controls](/docs/browsers/computer-controls) — the OS-level API that computer-use tools drive * [Computer Use overview](/docs/integrations/computer-use/overview) — running computer-use models on KERNEL more generally * [Replays](/docs/browsers/replays) — record a session end to end * [Stealth mode](/docs/browsers/bot-detection/stealth) — reduce how often a page notices the automation in the first place # Playwright Execution Source: https://kernel.sh/docs/browsers/playwright-execution Execute Playwright code in the same VM as your browser Execute arbitrary Playwright/TypeScript code in a fresh execution context against your browser. The code runs in the same VM as the browser, minimizing latency and maximizing throughput. **For complex workloads, Kernel has a full [code execution platform](/docs/apps)**. ## How it works When you execute Playwright code through this API: * Your code runs directly in the browser's VM (no CDP overhead) * You have access to `page`, `context`, and `browser` variables * You can `return` a value, which is returned in the response * Execution is isolated in a fresh context each time ## Quick example ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); // Create a browser const kernelBrowser = await kernel.browsers.create(); // Execute Playwright code const response = await kernel.browsers.playwright.execute( kernelBrowser.session_id, { code: ` await page.goto('https://example.com'); return await page.title(); ` } ); console.log(response.result); // "Example Domain" ``` ```python Python theme={null} from kernel import Kernel kernel = Kernel() # Create a browser kernel_browser = kernel.browsers.create() # Execute Playwright code response = kernel.browsers.playwright.execute( id=kernel_browser.session_id, code=""" await page.goto('https://example.com') return await page.title() """ ) print(response.result) # "Example Domain" ``` ```go Go theme={null} package main import ( "context" "fmt" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() // Create a browser kernelBrowser, err := client.Browsers.New(ctx, kernel.BrowserNewParams{}) if err != nil { panic(err) } // Execute Playwright code response, err := client.Browsers.Playwright.Execute(ctx, kernelBrowser.SessionID, kernel.BrowserPlaywrightExecuteParams{ Code: ` await page.goto('https://example.com'); return await page.title(); `, }) if err != nil { panic(err) } fmt.Println(response.Result) // "Example Domain" } ``` ```bash CLI theme={null} kernel browsers playwright execute 'await page.goto("https://www.onkernel.com"); return page.title();' ``` ## Available variables Your code has access to these Playwright objects: * `page` - The current page instance * `context` - The browser context * `browser` - The browser instance ## Returning values Use a `return` statement to send data back from your code: ```typescript Typescript/Javascript theme={null} const response = await kernel.browsers.playwright.execute( sessionId, { code: ` await page.goto('https://example.com'); const title = await page.title(); const url = page.url(); return { title, url }; ` } ); console.log(response.result); // { title: "Example Domain", url: "https://example.com" } ``` ```python Python theme={null} response = kernel.browsers.playwright.execute( id=session_id, code=""" await page.goto('https://example.com') title = await page.title() url = page.url() return {'title': title, 'url': url} """ ) print(response.result) # {'title': 'Example Domain', 'url': 'https://example.com'} ``` ```go Go theme={null} response, err := client.Browsers.Playwright.Execute(ctx, sessionID, kernel.BrowserPlaywrightExecuteParams{ Code: ` await page.goto('https://example.com'); const title = await page.title(); const url = page.url(); return { title, url }; `, }) if err != nil { panic(err) } fmt.Println(response.Result) // map[title:Example Domain url:https://example.com] ``` ## Timeout configuration Set a custom timeout (default is 60 seconds, max is 300 seconds): ```typescript Typescript/Javascript theme={null} const response = await kernel.browsers.playwright.execute( sessionId, { code: ` await page.goto('https://example.com'); return await page.title(); `, timeout_sec: 120 } ); ``` ```python Python theme={null} response = kernel.browsers.playwright.execute( id=session_id, code=""" await page.goto('https://example.com') return await page.title() """, timeout_sec=120 ) ``` ```go Go theme={null} response, err := client.Browsers.Playwright.Execute(ctx, sessionID, kernel.BrowserPlaywrightExecuteParams{ Code: ` await page.goto('https://example.com'); return await page.title(); `, TimeoutSec: kernel.Int(120), }) if err != nil { panic(err) } _ = response ``` ## Error handling The response includes error information if execution fails: ```typescript Typescript/Javascript theme={null} const response = await kernel.browsers.playwright.execute( sessionId, { code: ` await page.goto('https://invalid-url'); return await page.title(); ` } ); if (!response.success) { console.error('Error:', response.error); console.error('Stderr:', response.stderr); } ``` ```python Python theme={null} response = kernel.browsers.playwright.execute( id=session_id, code=""" await page.goto('https://invalid-url') return await page.title() """ ) if not response.success: print('Error:', response.error) print('Stderr:', response.stderr) ``` ```go Go theme={null} response, err := client.Browsers.Playwright.Execute(ctx, sessionID, kernel.BrowserPlaywrightExecuteParams{ Code: ` await page.goto('https://invalid-url'); return await page.title(); `, }) if err != nil { panic(err) } if !response.Success { fmt.Println("Error:", response.Error) fmt.Println("Stderr:", response.Stderr) } ``` ## Use cases ### Web scraping Extract data from multiple pages without CDP overhead: ```typescript theme={null} const response = await kernel.browsers.playwright.execute( sessionId, { code: ` await page.goto('https://news.ycombinator.com'); const titles = await page.$$eval('.titleline > a', links => links.map(link => link.textContent) ); return titles.slice(0, 10); ` } ); ``` ### Form automation Fill and submit forms quickly: ```typescript theme={null} const response = await kernel.browsers.playwright.execute( sessionId, { code: ` await page.goto('https://example.com/form'); await page.fill('#email', 'user@example.com'); await page.fill('#password', 'password123'); await page.click('button[type="submit"]'); await page.waitForNavigation(); return page.url(); ` } ); ``` ### Testing and validation Run quick checks against your browser state: ```typescript theme={null} const response = await kernel.browsers.playwright.execute( sessionId, { code: ` const cookies = await context.cookies(); const localStorage = await page.evaluate(() => JSON.stringify(window.localStorage) ); return { cookies, localStorage }; ` } ); ``` ### Screenshots Capture screenshots using Playwright's native screenshot API: ```typescript theme={null} const response = await kernel.browsers.playwright.execute( sessionId, { code: ` await page.goto('https://example.com'); const screenshot = await page.screenshot({ type: 'png', fullPage: true }); return screenshot.toString('base64'); ` } ); // Decode and save the screenshot const buffer = Buffer.from(response.result, 'base64'); fs.writeFileSync('screenshot.png', buffer); ``` For OS-level screenshots using coordinates and regions, see [Computer Controls](/docs/browsers/computer-controls#take-screenshots). ## Performance benefits Compared to connecting over CDP: * **Lower latency** - Code runs in the same VM as the browser * **Higher throughput** - No websocket overhead for commands * **Simpler code** - No need to manage CDP connections This makes it ideal for one-off operations where you need maximum speed. ## MCP server integration This feature is available as a tool in our [MCP server](/docs/reference/mcp-server). AI agents can use the `execute_playwright_code` tool to run Playwright code against browsers directly in the VM with lower latency. # Browser Pools Source: https://kernel.sh/docs/browsers/pools Configure a pool of ready-to-use browsers for instant acquisition A browser pool is a fixed set of identical browsers that Kernel keeps running for you. Configure it once — stealth, proxies, [private networking](/docs/browsers/private-networking), extensions, viewport, a [profile](#profiles-with-browser-pools) — then acquire a browser whenever a task needs one and release it when you're done. Acquiring is faster than creating an on-demand browser because the browser is already running: you skip start-up, including the [Chromium restart](/docs/browsers/performance#troubleshooting-latency) that some settings trigger, and you aren't subject to the [rate limit](/docs/info/pricing#rate-limiting) on browser creation. Idle browsers in a pool aren't billed, but the pool's capacity counts against your [concurrency limit](/docs/info/pricing#concurrency-limits). See [Scale](/docs/introduction/scale) for how browser pools fit into production architecture. ## How browser pools work Create a browser pool with your configuration and a fixed size. It takes a few minutes to fill, so declare it at deploy time or on startup rather than in the path that serves your workload. `acquire` returns a ready browser immediately, or waits until one frees up. A browser pool holds a fixed number of browsers and doesn't add extras to cover the ones in use: acquiring lowers its available count, releasing raises it again. Release the browser when you're done. Until you do — or until it [times out](#timeout-behavior) — it stays `acquired` and out of the pool. ## Limitations A few constraints to weigh before moving a workload onto a browser pool: * **No GPU browsers.** GPU-accelerated browsers are on-demand only. Use `browsers.create()` for WebGL, video, or canvas-heavy work. * **One fixed configuration per browser pool**, with `start_url` the only setting you can override per acquisition — see [Create a browser pool](#create-a-browser-pool). * **A profile set on the browser pool loads read-only**, and a browser pool holds one at a time — see [Per-user profiles with browser pools](#per-user-profiles-with-browser-pools) for how to persist state per user. * **Browser pool capacity counts against your [concurrency limit](/docs/info/pricing#concurrency-limits)** whether or not its browsers are acquired, though idle pooled browsers aren't billed. * **Plan-gated.** Browser pools are available on the Start-Up and Enterprise plans. ## Create a browser pool Create a browser pool with a size and the configuration every browser in it should use. ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); const pool = await kernel.browserPools.create({ name: "my-pool", size: 10, stealth: true, headless: false, timeout_seconds: 600, start_url: "https://example.com", viewport: { width: 1920, height: 1080 } }); console.log(pool.id); ``` ```python Python theme={null} from kernel import Kernel kernel = Kernel() pool = kernel.browser_pools.create( name="my-pool", size=10, stealth=True, headless=False, timeout_seconds=600, start_url="https://example.com", viewport={ "width": 1920, "height": 1080 } ) print(pool.id) ``` ```go Go theme={null} package main import ( "context" "fmt" "github.com/kernel/kernel-go-sdk" "github.com/kernel/kernel-go-sdk/shared" ) func main() { ctx := context.Background() client := kernel.NewClient() pool, err := client.BrowserPools.New(ctx, kernel.BrowserPoolNewParams{ Name: kernel.String("my-pool"), Size: 10, Stealth: kernel.Bool(true), Headless: kernel.Bool(false), TimeoutSeconds: kernel.Int(600), StartURL: kernel.String("https://example.com"), Viewport: shared.BrowserViewportParam{ Width: 1920, Height: 1080, }, }) if err != nil { panic(err) } fmt.Println(pool.ID) } ``` ## Acquire a browser Acquire a browser from the browser pool. The request returns immediately if a browser is available, or waits until one becomes available. Use `acquire_timeout_seconds` to bound that wait. ```typescript Typescript/Javascript theme={null} const browser = await kernel.browserPools.acquire("my-pool", { acquire_timeout_seconds: 30, }); console.log(browser.session_id); console.log(browser.cdp_ws_url); ``` ```python Python theme={null} browser = kernel.browser_pools.acquire( "my-pool", acquire_timeout_seconds=30, ) print(browser.session_id) print(browser.cdp_ws_url) ``` ```go Go theme={null} browser, err := client.BrowserPools.Acquire(ctx, "my-pool", kernel.BrowserPoolAcquireParams{ AcquireTimeoutSeconds: kernel.Int(30), }) if err != nil { panic(err) } fmt.Println(browser.SessionID) fmt.Println(browser.CdpWsURL) ``` The acquired browser exposes the same properties as an on-demand browser, including `cdp_ws_url` for CDP connections and `browser_live_view_url` for live viewing. ## Release a browser When you're done with a browser, release it back to the browser pool. By default, the browser instance is reused. Set `reuse: false` to destroy it and create a fresh one. Reuse controls whether browser state carries across acquisitions: * **`reuse: true`** (default) returns the same browser instance to the pool without resetting it. Cookies, local storage, logged-in sessions, and open tabs persist, and the next caller to acquire it inherits that state. Fast, but not isolated between acquirers. * **`reuse: false`** destroys the browser and refills the pool with a fresh one. Isolated and clean, at the cost of a rebuild. If you acquire pooled browsers on behalf of different end users, releasing with `reuse: true` leaks one user's session to the next. Use `reuse: false` for per-user work — see [Per-user profiles with browser pools](#per-user-profiles-with-browser-pools). ```typescript Typescript/Javascript theme={null} await kernel.browserPools.release("my-pool", { session_id: browser.session_id, reuse: true, }); ``` ```python Python theme={null} kernel.browser_pools.release( "my-pool", session_id=browser.session_id, reuse=True, ) ``` ```go Go theme={null} if err := client.BrowserPools.Release(ctx, "my-pool", kernel.BrowserPoolReleaseParams{ SessionID: browser.SessionID, Reuse: kernel.Bool(true), }); err != nil { panic(err) } ``` ### Timeout behavior Browsers wait in the browser pool indefinitely until acquired — a browser pool's `timeout_seconds` only starts running once a browser is acquired. From there it behaves like a [regular browser timeout](/docs/browsers/termination#automatic-deletion-via-timeout): if the browser sits idle, with no CDP or live view connection, for longer than the timeout, it's destroyed rather than returned to the pool, and the pool creates a replacement. As a best practice, release each browser when you're done with it — that returns it to the pool right away. The timeout is there as a backstop for browsers that never get released. ## Profiles with browser pools A [profile](/docs/auth/profiles) carries login state, including cookies and local storage, into a browser. Use [Managed Auth](/docs/auth/overview) to populate that state and monitor its health. Put the profile on the browser pool when every browser should share one identity; leave it off and attach it after acquiring when each task needs its own (see [Per-user profiles with browser pools](#per-user-profiles-with-browser-pools)). A profile attached to the pool is loaded **read-only**. Every browser in the pool shares it, so `save_changes` doesn't apply and is silently ignored if sent — this prevents concurrent writes from corrupting the profile. ### Per-user profiles with browser pools Because that profile is shared and read-only, it can't hold per-user login state for many users at once. To serve many users from one browser pool, create it with no profile — stealth, proxies, extensions, and viewport still live on the pool — then attach each user's profile to the browser *after* you acquire it, and release with `reuse: false` so the browser is destroyed. Destroying it keeps that user's state from reaching the next acquirer. The read-only rule covers the pool's own profile, not one attached after acquiring: that profile belongs to the browser, so `save_changes` applies as it does on any other browser. Pass `save_changes: true` when you attach it — it defaults to `false`, and without it the browser is destroyed on release without writing the user's session back. ```typescript Typescript/Javascript theme={null} const browser = await kernel.browserPools.acquire("my-pool"); await kernel.browsers.update(browser.session_id, { profile: { name: "user-8f21c3", save_changes: true } }); // ... drive the browser as that user ... await kernel.browserPools.release("my-pool", { session_id: browser.session_id, reuse: false, }); ``` ```python Python theme={null} browser = kernel.browser_pools.acquire("my-pool") kernel.browsers.update( browser.session_id, profile={"name": "user-8f21c3", "save_changes": True}, ) # ... drive the browser as that user ... kernel.browser_pools.release( "my-pool", session_id=browser.session_id, reuse=False, ) ``` ```go Go theme={null} browser, err := client.BrowserPools.Acquire(ctx, "my-pool", kernel.BrowserPoolAcquireParams{}) if err != nil { panic(err) } if _, err := client.Browsers.Update(ctx, browser.SessionID, kernel.BrowserUpdateParams{ Profile: shared.BrowserProfileParam{ Name: kernel.String("user-8f21c3"), SaveChanges: kernel.Bool(true), }, }); err != nil { panic(err) } // ... drive the browser as that user ... if err := client.BrowserPools.Release(ctx, "my-pool", kernel.BrowserPoolReleaseParams{ SessionID: browser.SessionID, Reuse: kernel.Bool(false), }); err != nil { panic(err) } ``` A profile can only be loaded into a browser that was created without one, which is why the pool itself has to stay profile-free. ### Refresh on profile update Each browser loads the profile's data at the moment it's created, so re-saving that profile later doesn't reach browsers that are already running. With `refresh_on_profile_update` enabled, saving the profile — after a [Managed Auth](/docs/auth/overview) login, for example — flushes every idle browser in the pool and replaces it with one that loads the updated data. Browsers that are currently acquired keep the data they started with. It's enabled automatically when a browser pool is created with a profile or has its profile changed, and forced to `false` when the profile is removed (by passing `{ "id": "" }`). Set it to `false` to opt out. `refresh_on_profile_update` requires a profile on the pool. Setting it to `true` without one returns a validation error. ## Sizing a browser pool `retrieve` reports how many browsers are ready to acquire right now (`available_count`) and how many are in use (`acquired_count`). Watch the available count under normal load: * **Regularly at zero.** Tasks are queueing behind `acquire` — the pool is too small. * **Steady at 10–20%.** The target: enough headroom to absorb a spike without reserving capacity you don't use. * **Consistently above 30–40%.** You're holding concurrency for browsers you never acquire, so shrink the pool. Resize at any time — no need to tear the pool down and rebuild it. Added capacity fills at the pool's [fill rate](https://kernel.sh/docs/api-reference/browser-pools/create-a-browser-pool#body-fill-rate-per-minute), so raise the size before a traffic peak rather than during it. Browser pools shine at higher concurrency, but even a small one is worth it when acquisition latency matters and demand is steady enough to keep it utilized — for example, a user-facing action where a cold start is unacceptable. On-demand browsers remain the better fit for low-volume, bursty, or one-off work, for configuration that varies per session, and for GPU browsers. ## Update a browser pool Update the browser pool's configuration. By default, existing idle browsers keep their current configuration and only newly created browsers use the new one. Pass `discard_all_idle: true` to discard all idle browsers and rebuild them immediately with the new configuration. ```typescript Typescript/Javascript theme={null} const updatedPool = await kernel.browserPools.update("my-pool", { size: 20, stealth: true, }); ``` ```python Python theme={null} updated_pool = kernel.browser_pools.update( "my-pool", size=20, stealth=True, ) ``` ```go Go theme={null} updatedPool, err := client.BrowserPools.Update(ctx, "my-pool", kernel.BrowserPoolUpdateParams{ Size: kernel.Int(20), Stealth: kernel.Bool(true), }) if err != nil { panic(err) } _ = updatedPool ``` Browsers that are acquired during an update are never touched, even with `discard_all_idle: true`. An in-use browser keeps its original configuration, and if you release it with `reuse: true` (the default) it re-enters the pool still carrying that configuration and keeps getting handed out that way. You have two ways to get an in-use browser onto the new configuration: * **Prevent it on release:** release with `reuse: false`. The browser is destroyed and rebuilt with the browser pool's current configuration instead of the old one returning to the pool. * **Clean it up after the fact:** `flush()` the pool, or run a later `update()` with `discard_all_idle: true`, once the in-use browsers have been released. Both destroy every idle browser and let the pool refill on the current configuration; neither touches a browser that's currently acquired, so both are safe to call under load. ## API reference See the [Browser Pools API reference](https://kernel.sh/docs/api-reference/browser-pools/list-browser-pools) for `retrieve`, `list`, `flush`, `delete`, and the full parameters and response shapes of every endpoint. # Private Networking Source: https://kernel.sh/docs/browsers/private-networking Route browser traffic to private services through a VPN or tunnel in the browser session Use `network.private_hosts` when a browser session joins a VPN or tunnel and must reach private services through that connection. Matching destinations bypass Kernel-managed egress and use the session's network routes and DNS instead. This is useful for services reachable through Tailscale, a corporate VPN, or another tunnel running inside the browser session. `network.private_hosts` is different from a proxy's [`bypass_hosts`](/docs/proxies/overview#bypass-hosts). Proxy bypass rules choose between your upstream proxy and Kernel-managed direct egress. Private hosts bypass Kernel-managed egress so traffic can follow routes inside the browser session, including VPN and tunnel routes. ## Configure a browser Set private hosts when you create the browser. You can't change the network configuration after creation. ```typescript TypeScript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); const browser = await kernel.browsers.create({ network: { private_hosts: [ '*.services.example.ts.net', '100.64.0.0/10', ], }, }); console.log(browser.session_id); ``` ```python Python theme={null} from kernel import Kernel kernel = Kernel() browser = kernel.browsers.create( network={ "private_hosts": [ "*.services.example.ts.net", "100.64.0.0/10", ] } ) print(browser.session_id) ``` ```go Go theme={null} package main import ( "context" "fmt" "github.com/kernel/kernel-go-sdk" ) func main() { client := kernel.NewClient() browser, err := client.Browsers.New(context.Background(), kernel.BrowserNewParams{ Network: kernel.BrowserNetworkConfigParam{ PrivateHosts: []string{ "*.services.example.ts.net", "100.64.0.0/10", }, }, }) if err != nil { panic(err) } fmt.Println(browser.SessionID) } ``` ```bash CLI theme={null} kernel browsers create \ --private-host '*.services.example.ts.net' \ --private-host '100.64.0.0/10' ``` ## Default private routes When you omit `network.private_hosts`, Kernel routes these private IP ranges through the session network by default: * RFC1918: `10.0.0.0/8`, `172.16.0.0/12`, and `192.168.0.0/16` * CGNAT and Tailscale: `100.64.0.0/10` * IPv6 unique local addresses: `fc00::/7` These CIDR rules only match URLs that use literal IP addresses. They don't match a hostname after DNS resolution. Add private DNS names explicitly, even when they resolve to an address in a default range: ```json theme={null} { "network": { "private_hosts": ["api.services.example.ts.net"] } } ``` Providing `private_hosts` replaces the default list; it doesn't add to it. Include any default CIDRs you still need alongside your hostname rules. To disable direct private routing and send all traffic through Kernel-managed egress, provide an explicit empty list: ```typescript TypeScript theme={null} const browser = await kernel.browsers.create({ network: { private_hosts: [] }, }); ``` ```python Python theme={null} browser = kernel.browsers.create( network={"private_hosts": []} ) ``` ## Supported entries You can provide up to 32 entries, each no longer than 255 characters: * Exact hostnames: `api.services.example.ts.net` * A single leading wildcard: `*.services.example.ts.net` * Private IPv4 addresses: `10.1.30.63` * Bracketed private IPv6 addresses: `[fd00::1]` * Canonical private CIDRs: `100.64.0.0/10` or `fd00::/8` * Hostnames or exact IP addresses with ports: `api.services.example.ts.net:8443` Kernel rejects public IP ranges, loopback and link-local ranges, URL schemes, paths, catch-all wildcards, ports on CIDRs, and non-canonical CIDRs. Hostnames aren't resolved during validation, so only add names that identify private destinations. ## Configure a browser pool Put the network configuration on a browser pool when every browser in the pool needs the same private routes. ```typescript TypeScript theme={null} const pool = await kernel.browserPools.create({ name: 'private-services', size: 5, network: { private_hosts: ['*.services.example.ts.net'], }, }); ``` ```python Python theme={null} pool = kernel.browser_pools.create( name="private-services", size=5, network={ "private_hosts": ["*.services.example.ts.net"], }, ) ``` ```bash CLI theme={null} kernel browser-pools create private-services \ --size 5 \ --private-host '*.services.example.ts.net' ``` A browser-pool update applies only to browsers created after the update. Pass `discard_all_idle: true` in an SDK request, or `--discard-all-idle` in the CLI, to immediately replace idle browsers with the new configuration. Acquired browsers keep their original configuration until you release them with reuse disabled. Use `kernel browser-pools update private-services --clear-private-hosts --discard-all-idle` to remove a pool override and restore the default private IP ranges. To configure an explicit empty list, use an SDK request with `network.private_hosts: []`. # Replays Source: https://kernel.sh/docs/browsers/replays Record and view browser sessions as mp4 videos Replays capture browser sessions as video recordings that you can view or download later. You have full control over when replays start and stop, allowing you to capture specific interactions or workflows. ## Starting and stopping recordings To start recording a browser session, use the replays API on an active browser: ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); const kernelBrowser = await kernel.browsers.create(); const replay = await kernel.browsers.replays.start(kernelBrowser.session_id); console.log(`Recording started with ID: ${replay.replay_id}`); // Perform some automation... await kernel.browsers.replays.stop(replay.replay_id, { id: kernelBrowser.session_id }); console.log('Recording stopped and processing'); ``` ```python Python theme={null} from kernel import Kernel kernel = Kernel() kernel_browser = kernel.browsers.create() replay = kernel.browsers.replays.start(kernel_browser.session_id) print(f"Recording started with ID: {replay.replay_id}") # Perform some automation... kernel.browsers.replays.stop(replay_id=replay.replay_id, id=kernel_browser.session_id) print("Recording stopped and processing") ``` ```go Go theme={null} package main import ( "context" "fmt" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() kernelBrowser, err := client.Browsers.New(ctx, kernel.BrowserNewParams{}) if err != nil { panic(err) } replay, err := client.Browsers.Replays.Start(ctx, kernelBrowser.SessionID, kernel.BrowserReplayStartParams{}) if err != nil { panic(err) } fmt.Printf("Recording started with ID: %s\n", replay.ReplayID) // Perform some automation... if err := client.Browsers.Replays.Stop(ctx, replay.ReplayID, kernel.BrowserReplayStopParams{ ID: kernelBrowser.SessionID, }); err != nil { panic(err) } fmt.Println("Recording stopped and processing") } ``` ## Multiple recordings per session You can create multiple replay recordings for a single browser session. Each recording gets a unique `replay_id` and can be started and stopped independently: ```typescript Typescript/Javascript theme={null} const replay1 = await kernel.browsers.replays.start(kernelBrowser.session_id); // Perform some automation... await kernel.browsers.replays.stop(replay1.replay_id, { id: kernelBrowser.session_id }); const replay2 = await kernel.browsers.replays.start(kernelBrowser.session_id); // Perform different automation... await kernel.browsers.replays.stop(replay2.replay_id, { id: kernelBrowser.session_id }); ``` ```python Python theme={null} replay1 = kernel.browsers.replays.start(kernel_browser.session_id) # Perform some automation... kernel.browsers.replays.stop(replay_id=replay1.replay_id, id=kernel_browser.session_id) replay2 = kernel.browsers.replays.start(kernel_browser.session_id) # Perform different automation... kernel.browsers.replays.stop(replay_id=replay2.replay_id, id=kernel_browser.session_id) ``` ```go Go theme={null} replay1, err := client.Browsers.Replays.Start(ctx, kernelBrowser.SessionID, kernel.BrowserReplayStartParams{}) if err != nil { panic(err) } // Perform some automation... if err := client.Browsers.Replays.Stop(ctx, replay1.ReplayID, kernel.BrowserReplayStopParams{ ID: kernelBrowser.SessionID, }); err != nil { panic(err) } replay2, err := client.Browsers.Replays.Start(ctx, kernelBrowser.SessionID, kernel.BrowserReplayStartParams{}) if err != nil { panic(err) } // Perform different automation... if err := client.Browsers.Replays.Stop(ctx, replay2.ReplayID, kernel.BrowserReplayStopParams{ ID: kernelBrowser.SessionID, }); err != nil { panic(err) } ``` ## Downloading all replays To access all replays for a browser session, list and access them via url or as downloads: ```typescript Typescript/Javascript theme={null} import fs from 'fs'; import { Buffer } from 'buffer'; const replays = await kernel.browsers.replays.list(kernelBrowser.session_id); for (const replay of replays) { console.log(`Replay ID: ${replay.replay_id}`); console.log(`View URL: ${replay.replay_view_url}`); const videoData = await kernel.browsers.replays.download( replay.replay_id, { id: kernelBrowser.session_id } ); const content = await videoData.blob(); const buffer = Buffer.from(await content.arrayBuffer()); const filename = `replay-${replay.replay_id}-${kernelBrowser.session_id}.mp4`; fs.writeFileSync(filename, buffer); } ``` ```python Python theme={null} import aiofiles replays = kernel.browsers.replays.list(kernel_browser.session_id) for replay in replays: print(f"Replay ID: {replay.replay_id}") print(f"View URL: {replay.replay_view_url}") video_data = kernel.browsers.replays.download( replay_id=replay.replay_id, id=kernel_browser.session_id ) content = video_data.read() filename = f"replay-{replay.replay_id}-{kernel_browser.session_id}.mp4" async with aiofiles.open(filename, 'wb') as f: await f.write(content) print(f"Saved replay to {filename}") ``` ```go Go theme={null} package main import ( "context" "fmt" "io" "os" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() sessionID := "brw_01jwv4tn5m8k3q2v7x9p0a1bc2" replays, err := client.Browsers.Replays.List(ctx, sessionID) if err != nil { panic(err) } for _, replay := range *replays { fmt.Printf("Replay ID: %s\n", replay.ReplayID) fmt.Printf("View URL: %s\n", replay.ReplayViewURL) videoData, err := client.Browsers.Replays.Download(ctx, replay.ReplayID, kernel.BrowserReplayDownloadParams{ ID: sessionID, }) if err != nil { panic(err) } filename := fmt.Sprintf("replay-%s-%s.mp4", replay.ReplayID, sessionID) file, err := os.Create(filename) if err != nil { videoData.Body.Close() panic(err) } _, copyErr := io.Copy(file, videoData.Body) closeErr := videoData.Body.Close() fileErr := file.Close() if copyErr != nil { panic(copyErr) } if closeErr != nil { panic(closeErr) } if fileErr != nil { panic(fileErr) } fmt.Printf("Saved replay to %s\n", filename) } } ``` ## Embedding a replay Set an iframe's `src` to the `replay_view_url` returned when you start or list a replay. Give the iframe an explicit aspect ratio so the player keeps its shape while loading, and enable fullscreen playback. Merge the example's `frame-src` sources into your existing Content Security Policy. Replace `REPLAY_VIEW_URL` with the URL returned by the API: ```html theme={null}
``` Use the recorded browser's viewport ratio if it differs from `16 / 10`. Treat replay URLs as sensitive. # SSH Access Source: https://kernel.sh/docs/browsers/ssh Open an interactive SSH session to a browser VM SSH into a running Kernel browser VM for debugging, running commands, or setting up port forwarding. ## Forward local dev server to browser A common use case is exposing a local development server to the remote Kernel browser. This lets the browser access `localhost` URLs that point to your local machine: ```bash theme={null} # 1. Start your local dev server (e.g., on port 3000) npm run dev # 2. Create a browser with extended timeout kernel browsers create --timeout 600 # 3. Forward your local server to the VM # This exposes localhost:3000 on your machine as localhost:3000 inside the VM kernel browsers ssh -R 3000:localhost:3000 # 4. In the browser's live view, navigate to: # http://localhost:3000 ``` Kernel detects browser activity via WebRTC (live view) or CDP connections. SSH connections alone don't count as activity, so without `--timeout`, your browser may be cleaned up while you're connected via SSH. Either set a timeout or keep the [live view](/docs/browsers/live-view) open. ## Prerequisites The `kernel browsers ssh` command requires [websocat](https://github.com/vi/websocat) to be installed locally: ```bash theme={null} brew install websocat ``` ```bash theme={null} curl -fsSL https://github.com/vi/websocat/releases/download/v1.14.1/websocat.x86_64-unknown-linux-musl \ -o /usr/local/bin/websocat && chmod +x /usr/local/bin/websocat ``` ## Basic usage Open an interactive SSH shell to a browser VM: ```bash theme={null} kernel browsers ssh ``` By default, this generates an ephemeral ed25519 SSH keypair for the session. The keypair is automatically cleaned up when the session ends. ## Using an existing SSH key Specify an existing SSH private key instead of generating an ephemeral one: ```bash theme={null} kernel browsers ssh -i ~/.ssh/id_ed25519 ``` The corresponding `.pub` file must exist alongside the private key (e.g., `~/.ssh/id_ed25519.pub`). ## Port forwarding Port forwarding uses standard SSH syntax. ### Local forwarding (`-L`) Forward a local port to a port on the VM. Useful for accessing services running inside the VM from your local machine: ```bash theme={null} # Access VM's port 5432 (e.g., a database) on local port 5432 kernel browsers ssh -L 5432:localhost:5432 ``` ### Remote forwarding (`-R`) Forward a VM port to a port on your local machine. Useful for exposing a local development server to the browser: ```bash theme={null} # Expose local dev server (port 3000) on VM port 8080 kernel browsers ssh -R 8080:localhost:3000 ``` This allows code running in the browser to access `localhost:8080` and reach your local development server. ## Setup only Configure SSH on the VM without opening a connection: ```bash theme={null} kernel browsers ssh --setup-only ``` This installs and configures the SSH server on the VM, then prints the manual connection command. Useful if you want to connect with your own SSH client or configuration. ## Flags | Flag | Description | | ----------------------------- | -------------------------------------------------------------- | | `-i, --identity ` | Path to SSH private key (generates ephemeral if not provided). | | `-L, --local-forward ` | Local port forwarding (`localport:host:remoteport`). | | `-R, --remote-forward ` | Remote port forwarding (`remoteport:host:localport`). | | `--setup-only` | Setup SSH on VM without connecting. | # Standby Mode Source: https://kernel.sh/docs/browsers/standby Kernel browsers enter standby mode during periods of inactivity. When a browser goes into standby mode, the browser's state remains the same but incurs zero usage costs. Kernel browsers automatically enter standby after `five seconds` with no activity. After it enters standby, the browser's [timeout](/docs/browsers/termination#automatic-deletion-via-timeout) countdown begins. A browser is considered active while any of the following is happening: * A CDP client is connected (Playwright, Puppeteer, or a raw CDP client) * A WebDriver/BiDi client is connected * A [Live View](/docs/browsers/live-view) client is connected * A [computer controls](/docs/browsers/computer-controls) API request is in flight (clicks, keypresses, screenshots, etc.) Any of the above resets the standby idle timer. As soon as none are active for five seconds, the browser enters standby. See [here](/docs/browsers/termination) to learn about destroying browsers. GPU-accelerated browsers do not support standby mode. # Telemetry Categories Source: https://kernel.sh/docs/browsers/telemetry/categories The categories a browser session can capture, what each contains, and their cost A category groups related telemetry events and is the unit you enable or disable. Selection is opt-in: a session captures a category only when you turn it on. For the full payload schema of any event type, see the [Stream telemetry events](https://kernel.sh/docs/api-reference/browser-telemetry/stream-telemetry-events-via-sse) endpoint in the API reference. ## Operational These categories report on the session itself rather than page content. | Category | Captures | Event types | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | | `control` | Actions that drive the browser: computer-control calls, Playwright code execution, screenshots and clipboard access, plus browser-control commands sent over the CDP proxy | `api_call`, `cdp_command` | | `platform` | Calls that manage the VM rather than drive the browser: recording lifecycle, filesystem, process execution, log streaming, scale-to-zero, telemetry, display and browser configuration, and extension uploads | `platform_api_call` | | `connection` | CDP and live view connect/disconnect activity | `cdp_connect`, `cdp_disconnect`, `live_view_connect`, `live_view_disconnect` | | `system` | VM-level failures | `system_oom_kill`, `service_crashed` | | `captcha` | Solver task lifecycle and visible challenge outcomes | `captcha_solve_started`, `captcha_solve_result`, `captcha_challenge_result` | `control` answers "what did my agent do." `platform` is mostly Kernel acting on the VM on your behalf - saving a profile, capturing a replay, polling a recorder - so it is off by default even though the rest of this group is on. Enable it when you are debugging a profile save, a replay, or a session-setup step rather than the agent itself. `control` reports one `cdp_command` for each supported browser-control command it can classify from the CDP proxy - input gestures, navigation, dialogs, file selection, screenshots, and every command phase, including `mouseMoved`, `keyUp`, and `char`. It doesn't report arbitrary CDP traffic; general inspection traffic such as most DOM and Runtime commands isn't classified as browser control. The command stream isn't sampled, coalesced, or reordered. ### Reduce CDP command volume Use `control.cdp.excluded_methods` to omit high-volume methods such as `Input.dispatchMouseEvent` during a humanized cursor path or `Page.captureScreenshot` during a screencast: ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); const browser = await kernel.browsers.create({ telemetry: { browser: { control: { enabled: true, cdp: { excluded_methods: ['Input.dispatchMouseEvent', 'Page.captureScreenshot'], }, }, }, }, }); ``` ```python Python theme={null} from kernel import Kernel kernel = Kernel() browser = kernel.browsers.create( telemetry={ "browser": { "control": { "enabled": True, "cdp": { "excluded_methods": [ "Input.dispatchMouseEvent", "Page.captureScreenshot", ], }, }, }, }, ) ``` Exclusion affects telemetry only; the commands still reach the browser. On `cdp_disconnect`, `telemetry_excluded` counts configured exclusions. Treat a nonzero `telemetry_dropped` as a telemetry-loss signal rather than using it to reconstruct the missing command sequence; the browser commands themselves still reach the browser. ### Correlate captcha tasks and challenges A visible captcha challenge can require multiple solver tasks. When the identifiers are present, use `task_id` to pair a `captcha_solve_started` event with its terminal `captcha_solve_result`, and use `challenge_id` to group the tasks for the same visible challenge. A `challenge_id` can continue across a page reload when the same challenge episode continues; it doesn't indicate task order or completion. | Event type | Scope | Meaning | | -------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `captcha_solve_started` | Solver task | The solver accepted a task. This doesn't mean a solve is currently in flight. | | `captcha_solve_result` | Solver task | A task ended with `success`, `failure`, `timeout`, or `abandoned`. A successful task means the solver returned a usable solution, not that the visible challenge cleared. | | `captcha_challenge_result` | Visible challenge | When emitted, the challenge reached its overall `solved`, `failure`, `timeout`, or `abandoned` outcome. | Challenge results are currently emitted for visible reCAPTCHA v2, hCaptcha, and press-and-hold challenges. Other captcha types can emit task events without a challenge result. Telemetry delivery is best-effort and isn't ordered: a start can arrive after its result, and any event can be absent. When you need a challenge-level outcome, wait for a bounded interval and fall back to available task results and the current page state. Task events carry a `task_id`, but not all of them carry a `challenge_id`. Kernel sets `challenge_id` from tracking the challenge widget in the page - the same tracking that produces `captcha_challenge_result` - so a task the automatic solver runs without it reports a `task_id` only. Those task events can't be joined to a challenge result for the same page, even when one is emitted. Pair a start with its result on `task_id`, and use `challenge_id` for grouping only when it's there. Use each result event's `duration_ms` as the authoritative duration rather than calculating it from event timestamps. Challenge duration covers every solver attempt from when the challenge appeared until its terminal outcome. Challenge outcomes have these meanings: | Status | Meaning | | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `solved` | The page observed the challenge clear after a solver attempt. This doesn't prove the site accepted the solution or that the guarded action succeeded. | | `failure` | A terminal solver failure occurred, or all attempts ended while the challenge remained. | | `timeout` | The challenge-level wait budget expired while the challenge remained. | | `abandoned` | Observation ended without an attributable terminal challenge outcome. This includes a dismissed widget, a page unload, or a token appearing while multiple challenges from the same provider are open. In the last case, a task can report `success` while the challenge with the same `challenge_id` reports `abandoned`. | ## Browser activity These categories report what's happening in the page. Capturing any of them attaches a Chrome DevTools Protocol (CDP) collector to the session and produces highly granular page-level events. Capturing them adds overhead, so enable only the ones you need. | Category | Captures | Event types | | ------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `console` | Console output from the page | `console_log`, `console_error` | | `network` | Network requests, responses, and failures | `network_request`, `network_response`, `network_loading_failed`, `network_idle`, `proxy_error` | | `page` | Navigation and page lifecycle, including performance signals and renderer crashes | `page_navigation`, `page_dom_content_loaded`, `page_load`, `page_tab_opened`, `page_crashed`, `page_layout_shift`, `page_lcp`, `page_layout_settled`, `page_navigation_settled` | | `interaction` | Browser-native input in the page (clicks, keys, scroll) | `interaction_click`, `interaction_key`, `interaction_scroll_settled` | | `screenshot` | Periodic screenshots of the session | `monitor_screenshot` | `interaction` events are browser-native DOM events observed in the page, not calls to the [computer-control](/docs/browsers/computer-controls) API (those are reported by the `control` category). ### The monitor category `monitor` reports the health of the CDP collector itself: `monitor_disconnected`, `monitor_reconnected`, `monitor_reconnect_failed`, and `monitor_init_failed`. It isn't directly settable. It flows automatically whenever any of the browser-activity categories are captured. You can still [filter the stream](/docs/browsers/telemetry/streaming) by `monitor` to isolate these events. ## Data sensitivity Telemetry is off by default. The default set isn't limited to session metadata: `control` records the source you submit for Playwright execution and sanitized arguments for supported browser-control commands, while `captcha` can record the host and path of the page where a solve ran. The browser-activity categories capture what flows through the page, which is your own browser's data and can include credentials and personal information. | Category | Can contain sensitive data | | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `network` | Request and response headers (including `Authorization` and `Cookie`), request bodies, and truncated response bodies, plus full URLs. A common place for session tokens, credentials, and personal data. | | `console` | Anything the page logs. Applications often log access tokens, request or response bodies, and personal data through `console.log`. | | `page` | Page URLs and titles, which can embed tokens or identifiers in query strings or fragments. | | `interaction` | Text of clicked elements and typed keys, which can include personal data entered into forms. | | `screenshot` | A full rendered image of the page - the broadest exposure, capturing anything visible on screen. | | `control` | The source you submit to the Playwright code-execution endpoint, on the `code` field of `api_call`, capped at 8 KB and marked with `...[truncated]` when cut. Whatever your script embeds is captured with it, so a literal password or token in the snippet is captured too. `cdp_command` carries sanitized arguments such as the method and phase, coordinates, counts, flags, and named keys such as `Enter` and `Tab`. Typed text, file paths, scripts, templates, dialog input, and autofill values aren't captured; navigation commands retain only the URL scheme, not the host, path, query, or fragment. | | `captcha` | Captcha type, task and challenge outcomes and durations, and opaque `task_id` and `challenge_id` values. It can include the host and path of the page where a task ran or a challenge appeared; the query string is excluded. Failed tasks can include a solver-specific error code. | | `platform`, `connection`, `system`, `monitor` | Session and VM metadata only (VM-management calls, connection and health events). No page content. | Captured events are persisted and can be replayed by [resuming the stream](/docs/browsers/telemetry/streaming#resuming-after-a-disconnect), so this sensitivity applies to the data at rest, not just the live stream. Events are retained for 30 days, then expired (see [Retention](/docs/browsers/telemetry/overview#retention)). Treat captured telemetry - and anywhere you forward or store it - with the same care as the underlying content. For how Kernel encrypts, retains, and processes data overall, see [Security](/docs/security) and the [Data Processing Addendum](/docs/dpa). Some exposure is reduced for you automatically: input into sensitive fields such as passwords is suppressed (`interaction_key` isn't emitted for them, and `interaction_click` omits the element text), and `cdp_command` reports text lengths rather than the text itself. Beyond that, because selection is opt-in, the most effective control is to capture only the categories you need - enable `network`, `console`, `page`, `interaction`, or `screenshot` deliberately, and prefer the operational categories when you only need session health. If you capture `control` and run Playwright code, pass credentials in through variables your snippet reads rather than as literals in the submitted source, so the captured `code` doesn't carry them. If you operate under HIPAA, GDPR, or similar obligations, be deliberate about the browser-activity categories: pointing them at a site that handles regulated data captures that data into storage. If your organization has a BAA with Kernel, the `network`, `console`, and `screenshot` categories are disabled and can't be captured. `control` and `captcha` stay available; keep regulated values out of the Playwright source you submit, and disable `captcha` if the page host or path identifies regulated data. If you have compliance requirements around what Kernel may process, [contact us](mailto:security@kernel.sh) before enabling them. # Telemetry Overview Source: https://kernel.sh/docs/browsers/telemetry/overview Capture what happens inside a browser session Telemetry captures events from inside a browser session - console output, network activity, page lifecycle, user interactions, captcha solves, and operational signals like crashes or connection changes. Once enabled, you can [stream them](/docs/browsers/telemetry/streaming) live or pull them later for analysis. Events are grouped into categories (`console`, `network`, `page`, and so on), and categories are the unit of control. Selection is opt-in: a session captures only the categories you turn on, and anything you don't stays off. See [Categories](/docs/browsers/telemetry/categories) for the full list and what each one captures. Telemetry is a recent addition. If the `telemetry` options or `telemetry stream` command aren't available, upgrade to the latest CLI (`kernel upgrade`) and SDK (`@onkernel/sdk` for TypeScript, `kernel` for Python). ## Enabling telemetry You configure telemetry when you [create a browser](/docs/introduction/create) (and can change it later on update). There are three ways to configure it. ### Enable the default set Set `enabled: true` with no per-category settings to capture the default set - a lightweight bundle of operational signals (`control`, `connection`, `system`, `captcha`) that's cheap to leave on: ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); const browser = await kernel.browsers.create({ telemetry: { enabled: true }, }); ``` ```python Python theme={null} from kernel import Kernel kernel = Kernel() browser = kernel.browsers.create( telemetry={"enabled": True}, ) ``` ```bash CLI theme={null} kernel browsers create --telemetry=all ``` ### Capture specific categories List the categories you want under `telemetry.browser`. For example, this session captures `console` and `network` only: ```typescript Typescript/Javascript theme={null} const browser = await kernel.browsers.create({ telemetry: { browser: { console: { enabled: true }, network: { enabled: true }, }, }, }); ``` ```python Python theme={null} browser = kernel.browsers.create( telemetry={ "browser": { "console": {"enabled": True}, "network": {"enabled": True}, }, }, ) ``` ```bash CLI theme={null} kernel browsers create --telemetry=console,network ``` ### Disable telemetry Telemetry is disabled by default. Use this only when updating a session to turn previously enabled telemetry back off. Set `enabled: false` on an existing session to turn telemetry off: ```typescript Typescript/Javascript theme={null} await kernel.browsers.update(browser.session_id, { telemetry: { enabled: false }, }); ``` ```python Python theme={null} kernel.browsers.update( browser.session_id, telemetry={"enabled": False}, ) ``` ```bash CLI theme={null} kernel browsers update --telemetry=off ``` On update, a category list patches the current selection - categories you don't include keep their current state. To reset the selection instead, send `enabled: true` (it replaces the selection with the categories you provide, or the default set if you provide none); send `enabled: false` to turn telemetry off. ## Retention Captured events are retained for 30 days, then expired. You can [stream](/docs/browsers/telemetry/streaming) events live or pull them later for analysis within that window; after 30 days the events are no longer available. ## What's next * [Categories](/docs/browsers/telemetry/categories) - every category, what it captures, the default set, and cost characteristics. * [Stream telemetry](/docs/browsers/telemetry/streaming) - consume the live stream from the SDK, CLI, or raw SSE, with filtering and reconnection. * For event payload schemas, see the [Stream telemetry events](https://kernel.sh/docs/api-reference/browser-telemetry/stream-telemetry-events-via-sse) endpoint in the API reference. # Stream Telemetry Source: https://kernel.sh/docs/browsers/telemetry/streaming Consume a session's live telemetry stream from the SDK or CLI Once a session has telemetry [enabled](/docs/browsers/telemetry/overview), you can stream its events in real time. The stream stays open until the session terminates. ## Via SDK Open the stream and iterate over the envelopes: ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); const stream = await kernel.browsers.telemetry.stream(sessionId); for await (const { seq, event } of stream) { console.log(`#${seq} [${event.category}] ${event.type}`); } ``` ```python Python theme={null} from kernel import Kernel kernel = Kernel() with kernel.browsers.telemetry.stream(session_id) as stream: for envelope in stream: event = envelope.event print(f"#{envelope.seq} [{event.category}] {event.type}") ``` To filter, check `event.category` and `event.type` in your loop. If the stream drops, re-open it with the last `seq` you processed as `last_event_id` to resume without gaps. ## Via CLI Stream events to your terminal. The command runs until the session ends or you interrupt it: ```bash theme={null} kernel browsers telemetry stream ``` ### Filtering by category or event type ```bash theme={null} # Only network and console events kernel browsers telemetry stream --categories=network,console # Only specific event types kernel browsers telemetry stream --types=network_response,console_error # Machine-readable output # -o json emits newline-delimited JSON envelopes for piping: kernel browsers telemetry stream -o json ``` ### Resuming after a disconnect The stream is a single connection; it does not reconnect on its own. Each event carries a monotonic `seq`, so to resume without gaps you re-open the stream and pass the last `seq` you processed. ```bash theme={null} kernel browsers telemetry stream --seq 1024 ``` The server then replays events after that sequence number. # Termination & Timeouts Source: https://kernel.sh/docs/browsers/termination Kernel browsers should be terminated after you're done with them. Using Playwright/Puppeteer's method `browser.close()` does not delete the browser. Use one of the methods below to delete the browser. ## Deleting a browser via session ID Every browser instance has a `session_id`. You can delete any browser using its session ID: ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); await kernel.browsers.deleteByID('htzv5orfit78e1m2biiifpbv'); ``` ```python Python theme={null} from kernel import Kernel kernel = Kernel() kernel.browsers.delete_by_id("htzv5orfit78e1m2biiifpbv") ``` ```go Go theme={null} package main import ( "context" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() if err := client.Browsers.DeleteByID(ctx, "htzv5orfit78e1m2biiifpbv"); err != nil { panic(err) } } ``` ## Automatic deletion via timeout If you don't manually delete a browser, it will be automatically deleted after a configurable `timeout` (default 60 seconds). The timeout begins once the browser enters [standby](/docs/browsers/standby) — i.e. when there's no CDP or WebDriver client, no Live View viewer, and no [computer controls](/docs/browsers/computer-controls) request in flight. You can set a custom timeout of up to 72 hours when creating a browser: ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); const browser = await kernel.browsers.create({ timeout_seconds: 300 }); console.log(browser.session_id); ``` ```python Python theme={null} from kernel import Kernel kernel = Kernel() browser = kernel.browsers.create(timeout_seconds=300) print(browser.session_id) ``` ```go Go theme={null} package main import ( "context" "fmt" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() browser, err := client.Browsers.New(ctx, kernel.BrowserNewParams{ TimeoutSeconds: kernel.Int(300), }) if err != nil { panic(err) } fmt.Println(browser.SessionID) } ``` # Viewports Source: https://kernel.sh/docs/browsers/viewport Configure browser viewport size and refresh rate for your automations Kernel browsers allow you to configure the viewport size and refresh rate when creating a browser session. The viewport configuration determines the initial browser window dimensions and display refresh rate. The refresh rate can be explicitly specified or automatically determined based on the width and height if they match a supported configuration. ## Default viewport If the `viewport` parameter is omitted when creating a browser, the default configuration is 1920x1080 at 25Hz. ```typescript Typescript/Javascript theme={null} // Uses default viewport (1920x1080@25Hz) const defaultViewport = await kernel.browsers.create(); ``` ```python Python theme={null} # Uses default viewport (1920x1080@25Hz) default_viewport = kernel.browsers.create() ``` ```go Go theme={null} // Uses default viewport (1920x1080@25Hz) defaultViewport, err := client.Browsers.New(ctx, kernel.BrowserNewParams{}) if err != nil { panic(err) } _ = defaultViewport ``` ## Setting viewport configuration You can configure the viewport when creating a browser by specifying the `viewport` parameter with `width` and `height`. The `refresh_rate` is optional and will be automatically determined from the dimensions if they match a supported configuration: Setting a custom viewport configuration triggers a Chromium restart, which can take several seconds. Use [browser pools](/docs/browsers/pools) to access browsers with custom viewport configurations faster. ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); // Explicitly specify refresh rate const kernelBrowser = await kernel.browsers.create({ viewport: { width: 1920, height: 1080, refresh_rate: 25 } }); // Auto-determine refresh rate from dimensions (25Hz for 1920x1080) const kernelBrowserAuto = await kernel.browsers.create({ viewport: { width: 1920, height: 1080 } }); ``` ```python Python theme={null} from kernel import Kernel kernel = Kernel() # Explicitly specify refresh rate kernel_browser = kernel.browsers.create( viewport={ "width": 1920, "height": 1080, "refresh_rate": 25 } ) # Auto-determine refresh rate from dimensions (25Hz for 1920x1080) kernel_browser_auto = kernel.browsers.create( viewport={ "width": 1920, "height": 1080 } ) ``` ```go Go theme={null} package main import ( "context" "github.com/kernel/kernel-go-sdk" "github.com/kernel/kernel-go-sdk/shared" ) func main() { ctx := context.Background() client := kernel.NewClient() // Explicitly specify refresh rate kernelBrowser, err := client.Browsers.New(ctx, kernel.BrowserNewParams{ Viewport: shared.BrowserViewportParam{ Width: 1920, Height: 1080, RefreshRate: kernel.Int(25), }, }) if err != nil { panic(err) } _ = kernelBrowser // Auto-determine refresh rate from dimensions (25Hz for 1920x1080) kernelBrowserAuto, err := client.Browsers.New(ctx, kernel.BrowserNewParams{ Viewport: shared.BrowserViewportParam{ Width: 1920, Height: 1080, }, }) if err != nil { panic(err) } _ = kernelBrowserAuto } ``` The `refresh_rate` parameter only applies to live view sessions and is ignored for [headless](/docs/browsers/headless) browsers. ## Window size vs. page viewport The `viewport` parameter sets the dimensions of the browser **window**, not the visible page area. On headful browsers, Chromium's UI (tab strip and toolbar) occupies part of the window height, so the page renders in a slightly shorter area than the configured height. For example, with a 1280x800 viewport, `window.innerHeight` will be less than 800 and content near the bottom of the page may not be visible in screenshots or live view. If your automation expects an exact page viewport, either: * **Use kiosk mode** to remove the browser UI, so the window dimensions match the page viewport exactly: ```typescript Typescript/Javascript theme={null} const kernelBrowser = await kernel.browsers.create({ kiosk_mode: true, viewport: { width: 1920, height: 1080 } }); ``` ```python Python theme={null} kernel_browser = kernel.browsers.create( kiosk_mode=True, viewport={"width": 1920, "height": 1080} ) ``` * **Set the page viewport through your automation framework.** This will update the rendered page size, but will not be reflected in the live view or computer controls: ```typescript Typescript/Javascript theme={null} // Playwright await page.setViewportSize({ width: 1280, height: 800 }); ``` ```python Python theme={null} # Playwright await page.set_viewport_size({"width": 1280, "height": 800}) ``` * **Account for the browser UI when choosing dimensions** by adding its height to the `height` you pass to Kernel, so the remaining page area matches your target size. On [headless](/docs/browsers/headless) browsers there is no browser UI, so the page viewport matches the configured dimensions exactly. ## Supported viewport configurations Kernel supports specific viewport configurations tuned for optimal performance and Computer Use compatibility. When you provide width and height without specifying refresh\_rate, it will be automatically determined if the dimensions match one of the supported resolutions exactly. The following resolutions are supported: | Resolution | Width | Height | Refresh Rate | | ---------- | ----- | ------ | ------------ | | QHD | 2560 | 1440 | 10 Hz | | Full HD | 1920 | 1080 | 25 Hz | | WUXGA | 1920 | 1200 | 25 Hz | | WXGA+ | 1440 | 900 | 25 Hz | | WXGA | 1280 | 800 | 60 Hz | | WXGA | 1200 | 800 | 60 Hz | | XGA | 1024 | 768 | 60 Hz | | Tablet | 768 | 1024 | 60 Hz | | Mobile | 390 | 844 | 60 Hz | When specifying a viewport: * **Width** and **Height** are required * **Viewport configurations** outside of the supported list are available, but they are provided on a "use at your own risk" basis and may not work as expected. * **Refresh Rate** is optional - if omitted, it will be automatically determined from the width and height combination ## Example configurations ```typescript Typescript/Javascript theme={null} // Full HD (1920x1080) at 25Hz - explicit refresh rate const fullHD = await kernel.browsers.create({ viewport: { width: 1920, height: 1080, refresh_rate: 25 } }); // Full HD (1920x1080) - auto-determined 25Hz (Default configuration) const fullHDAuto = await kernel.browsers.create({ viewport: { width: 1920, height: 1080 } }); // QHD (2560x1440) - auto-determined 10Hz // Note: May affect live view responsiveness const qhd = await kernel.browsers.create({ viewport: { width: 2560, height: 1440 } }); // XGA (1024x768) - auto-determined 60Hz const xga = await kernel.browsers.create({ viewport: { width: 1024, height: 768 } }); // WUXGA (1920x1200) at 25Hz - explicit refresh rate const wuxga = await kernel.browsers.create({ viewport: { width: 1920, height: 1200, refresh_rate: 25 } }); ``` ```python Python theme={null} # Full HD (1920x1080) at 25Hz - explicit refresh rate full_hd = kernel.browsers.create( viewport={ "width": 1920, "height": 1080, "refresh_rate": 25 } ) # Full HD (1920x1080) - auto-determined 25Hz (Default configuration) full_hd_auto = kernel.browsers.create( viewport={ "width": 1920, "height": 1080 } ) # QHD (2560x1440) - auto-determined 10Hz # Note: May affect live view responsiveness qhd = kernel.browsers.create( viewport={ "width": 2560, "height": 1440 } ) # XGA (1024x768) - auto-determined 60Hz xga = kernel.browsers.create( viewport={ "width": 1024, "height": 768 } ) # WUXGA (1920x1200) at 25Hz - explicit refresh rate wuxga = kernel.browsers.create( viewport={ "width": 1920, "height": 1200, "refresh_rate": 25 } ) ``` ```go Go theme={null} // Full HD (1920x1080) at 25Hz - explicit refresh rate fullHD, err := client.Browsers.New(ctx, kernel.BrowserNewParams{ Viewport: shared.BrowserViewportParam{ Width: 1920, Height: 1080, RefreshRate: kernel.Int(25), }, }) if err != nil { panic(err) } _ = fullHD // Full HD (1920x1080) - auto-determined 25Hz (Default configuration) fullHDAuto, err := client.Browsers.New(ctx, kernel.BrowserNewParams{ Viewport: shared.BrowserViewportParam{ Width: 1920, Height: 1080, }, }) if err != nil { panic(err) } _ = fullHDAuto // QHD (2560x1440) - auto-determined 10Hz // Note: May affect live view responsiveness qhd, err := client.Browsers.New(ctx, kernel.BrowserNewParams{ Viewport: shared.BrowserViewportParam{ Width: 2560, Height: 1440, }, }) if err != nil { panic(err) } _ = qhd // XGA (1024x768) - auto-determined 60Hz xga, err := client.Browsers.New(ctx, kernel.BrowserNewParams{ Viewport: shared.BrowserViewportParam{ Width: 1024, Height: 768, }, }) if err != nil { panic(err) } _ = xga // WUXGA (1920x1200) at 25Hz - explicit refresh rate wuxga, err := client.Browsers.New(ctx, kernel.BrowserNewParams{ Viewport: shared.BrowserViewportParam{ Width: 1920, Height: 1200, RefreshRate: kernel.Int(25), }, }) if err != nil { panic(err) } _ = wuxga ``` ## Dynamically changing the viewport You can change the viewport of a browser after it has been created using the [update browser endpoint](https://kernel.sh/docs/api-reference/browsers/update-browser-session). ```typescript Typescript/Javascript theme={null} // Create a browser with default viewport const kernelBrowser = await kernel.browsers.create(); // Later, change the viewport await kernel.browsers.update(kernelBrowser.session_id, { viewport: { width: 1024, height: 768 } }); ``` ```python Python theme={null} # Create a browser with default viewport kernel_browser = await kernel.browsers.create() # Later, change the viewport await kernel.browsers.update(kernel_browser.session_id, viewport={"width": 1024, "height": 768}) ``` ```go Go theme={null} // Create a browser with default viewport kernelBrowser, err := client.Browsers.New(ctx, kernel.BrowserNewParams{}) if err != nil { panic(err) } // Later, change the viewport _, err = client.Browsers.Update(ctx, kernelBrowser.SessionID, kernel.BrowserUpdateParams{ Viewport: kernel.BrowserUpdateParamsViewport{ BrowserViewportParam: shared.BrowserViewportParam{ Width: 1024, Height: 768, }, }, }) if err != nil { panic(err) } ``` There are important limitations when changing the viewport: * **Headful browsers**: You can't resize the viewport while [live view](/docs/browsers/live-view) is active or while a [replay](/docs/browsers/replays) is actively recording unless you set the `force` flag to `true`. * **Headless browsers**: Changing the viewport triggers a Chromium restart, which takes several seconds and may disrupt active CDP connections. ### Force resizing during recording If you need to resize the viewport while a replay is actively recording, set `force` to `true` in the update request. This stops the current recording, resizes the viewport, and restarts the recording — resulting in multiple replay segments for the session. ```typescript Typescript/Javascript theme={null} await kernel.browsers.update(kernelBrowser.session_id, { viewport: { width: 1024, height: 768, force: true } }); ``` ```python Python theme={null} await kernel.browsers.update( kernel_browser.session_id, viewport={"width": 1024, "height": 768, "force": True} ) ``` ```go Go theme={null} _, err := client.Browsers.Update(ctx, kernelBrowser.SessionID, kernel.BrowserUpdateParams{ Viewport: kernel.BrowserUpdateParamsViewport{ BrowserViewportParam: shared.BrowserViewportParam{ Width: 1024, Height: 768, }, Force: kernel.Bool(true), }, }) if err != nil { panic(err) } ``` ## Considerations * The viewport configuration is set when the browser is created and applies to the initial browser window * Higher resolutions (like 2560x1440) may impact the performance and responsiveness of live view sessions * The viewport size affects how websites render, especially those with responsive designs * Screenshots taken through your automation framework capture the page viewport, which on headful browsers is shorter than the configured dimensions due to the browser UI (see [Window size vs. page viewport](#window-size-vs-page-viewport)) # Discord Source: https://kernel.sh/docs/community/discord # Github Source: https://kernel.sh/docs/community/github # cookbooks Source: https://kernel.sh/docs/cookbooks end-to-end recipes to teach agents to use the internet Self-contained cookbooks to clone and run. Use as 1-click starts for your agents or as templates for your own build.
common patterns
Enable Payments in a Browser Agent
harnesses & models
Browser automation with the Vercel AI SDK and Kernel's Playwright execution API.
harnesses & models
RL training for computer use agents, using Tinker.
harnesses & models
Human-in-the-loop web task assistant with memory, built on Mastra.
harnesses & models
Run parallel computer-use agent swarms, with API keys secured via Vaults.
harnesses & models
QA your PR previews with Claude computer use on Modal, recorded with Replays.
harnesses & models
Run the Browser Use bu-1.0 model on Kernel browser infrastructure.
common patterns
Using Playwright with Computer Use Fallback
harnesses & models
Computer use agent with Google's Gemini 2.5 and Stagehand.
harnesses & models
Vibium browser automation over WebDriver BiDi.
harnesses & models
Minimal implementation of Anthropic's computer use loop.
harnesses & models
Scrape JS-rendered pages behind a login, on Modal with a Kernel headful browser.
# AI SDK Agent Source: https://kernel.sh/docs/cookbooks/ai-sdk-agent # Browser Use Model Source: https://kernel.sh/docs/cookbooks/browser-use-model # Claude Computer Use Loop Source: https://kernel.sh/docs/cookbooks/claude-computer-use-loop # Claude Managed Agents Source: https://kernel.sh/docs/cookbooks/claude-managed-agents # Mastra Web Task Assistant Source: https://kernel.sh/docs/cookbooks/mastra-web-task-assistant # Modal PR QA Agent Source: https://kernel.sh/docs/cookbooks/modal-pr-qa-agent # Modal Web Scraper Source: https://kernel.sh/docs/cookbooks/modal-web-scraper # Stagehand Google CUA Agent Source: https://kernel.sh/docs/cookbooks/stagehand-google-cua-agent # Tinker RL Source: https://kernel.sh/docs/cookbooks/tinker-rl # Vibium Source: https://kernel.sh/docs/cookbooks/vibium # wrangle the wild web Source: https://kernel.sh/docs/index We build crazy fast, open source infra for AI agents to access the internet. Trusted by Cash App, Framer, and 3,000+ teams. We spin up cloud browsers in \<30ms with GPU acceleration when needed. We manage auth for your agents so you don't have to. We solve CAPTCHAs and manage residential proxies to help you see fewer of them. You can view sessions live and record them as MP4s for debugging. ## start here Spin up a browser and pick the shape — headless, stealth, GPU, profiles. Drive it with computer use, playwright execution, CDP, or WebDriver BiDi. Watch it live, record replays, and capture screenshots.

fast setup

copy and paste this into your AI coding agent (Cursor, Claude, Windsurf, etc.). it installs the Kernel CLI and skills, authenticates you, and opens a live browser session that you or your agent can interact with.

## prod setup Our [app platform](/docs/apps/develop) is a serverless compute service for running agent loops triggered on demand or by scheduled events without having to provision or manage sandboxes. Your agent runs co-located with its browser to minimize network latency. Scaffold a project from a template: ```bash theme={null} kernel create --template computer-use ``` Deploy and invoke it on demand: ```bash theme={null} kernel deploy agent.ts kernel invoke my-agent my-task --payload '{"url": "https://example.com"}' ``` ### scaling [browser pools](/docs/browsers/pools) keep browsers ready to use and pre-configured, so you skip start-up latency on every task and idle browsers aren't billed. reach for them once you're running the same workload repeatedly, need low-latency acquisition, or are scaling steady, high-frequency traffic — on-demand `browsers.create()` stays the right call for occasional, bursty, or one-off work. # API Keys Source: https://kernel.sh/docs/info/api-keys Create, scope, rotate, and delete Kernel API keys An API key is the credential your server, script, or CI job uses to call Kernel without an interactive login. Treat it like a password: keep it out of client-side code, store it in a secret manager, and rotate it when access changes. Kernel only returns the plaintext key once, when you create it. Save the `key` value immediately. After that, Kernel only shows the masked value. ## Before you start You need one existing Kernel credential to create another API key: * Set `KERNEL_API_KEY` before running the SDK examples. API keys can be **org** or **project** scoped: * Omit `project_id` to create an org-scoped key that can access resources across your organization. * Set `project_id` to create a project-scoped key that can only access resources in that project. * When you authenticate with a project-scoped key, you can only create another project-scoped key for the same project. ## Create an API key Use the SDKs when your backend needs to provision keys for environments, customers, or automation jobs. ### SDKs ```typescript TypeScript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel({ apiKey: process.env.KERNEL_API_KEY, }); const apiKey = await kernel.apiKeys.create({ name: 'staging-ci', days_to_expire: 30, project_id: 'proj_staging_9f3k', }); console.log(apiKey.key); // Save this value now. Kernel won't show it again. console.log(apiKey.id, apiKey.masked_key); ``` ```python Python theme={null} import os from kernel import Kernel client = Kernel(api_key=os.environ["KERNEL_API_KEY"]) api_key = client.api_keys.create( name="staging-ci", days_to_expire=30, project_id="proj_staging_9f3k", ) print(api_key.key) # Save this value now. Kernel won't show it again. print(api_key.id, api_key.masked_key) ``` ```go Go theme={null} package main import ( "context" "fmt" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() apiKey, err := client.APIKeys.New(ctx, kernel.APIKeyNewParams{ Name: "staging-ci", DaysToExpire: kernel.Int(30), ProjectID: kernel.String("proj_staging_9f3k"), }) if err != nil { panic(err) } fmt.Println(apiKey.Key) // Save this value now. Kernel won't show it again. fmt.Println(apiKey.ID, apiKey.MaskedKey) } ``` ## Deployment API keys When you deploy an app, Kernel mints a **deployment-scoped API key** for that deployment and injects it into the deployment (and every invocation it runs) as the `KERNEL_API_KEY` environment variable. Because the SDKs read `KERNEL_API_KEY` from the environment by default, your app can call the Kernel API as itself without you managing a key. Key points about deployment keys: * **One key per deployment.** Each deploy (including a redeploy of the same app) mints a fresh deployment key. `KERNEL_API_KEY` is a [reserved environment variable](/docs/apps/deploy#reserved-environment-variables) — a value you supply at deploy time is overridden by the injected key. To use your own long-lived key, pass it under a non-reserved name. * **Lifecycle tied to the deployment.** A deployment key stays valid while its deployment is active. When you redeploy, the new deployment supersedes the old one, and the old deployment's key is released once it is no longer needed — that is, once the superseded deployment is stopped **and** no invocation is still running on it. * **In-flight invocations are drained, not cut off.** If an invocation is still running on a deployment that gets superseded, its key is kept valid until that invocation completes; the key is released right after. An idle redeploy (nothing in flight) releases the old key immediately. In the rare case where an invocation's workflow terminates without releasing the key, a background sweep releases it after a grace period (\~95 minutes). You do not need to manage any of this — it is automatic. If you need a credential whose lifetime you control (for CI, a persistent backend, or a resource that should outlive any single deployment), create an org- or project-scoped key as described below and pass it explicitly to the client. This only affects the calls your app makes — the platform still injects the deployment key and uses it to run the invocation and report its result, so you're choosing the credential for your own calls, not replacing the deployment key. ## List and inspect API keys List keys to audit what exists. List and retrieve responses include `masked_key`, `project_id`, `project_name`, `created_by`, and expiry metadata, but they don't include the plaintext key. ```typescript TypeScript theme={null} for await (const apiKey of kernel.apiKeys.list({ limit: 20 })) { console.log(apiKey.id, apiKey.name, apiKey.masked_key); } const apiKey = await kernel.apiKeys.retrieve('key_01jwv4tn5m8k3q2v7x9p0a1bc2'); console.log(apiKey.project_id, apiKey.expires_at); ``` ```python Python theme={null} import os from kernel import Kernel client = Kernel(api_key=os.environ["KERNEL_API_KEY"]) for api_key in client.api_keys.list(limit=20): print(api_key.id, api_key.name, api_key.masked_key) api_key = client.api_keys.retrieve("key_01jwv4tn5m8k3q2v7x9p0a1bc2") print(api_key.project_id, api_key.expires_at) ``` ```go Go theme={null} package main import ( "context" "fmt" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() pager := client.APIKeys.ListAutoPaging(ctx, kernel.APIKeyListParams{ Limit: kernel.Int(20), }) for pager.Next() { apiKey := pager.Current() fmt.Println(apiKey.ID, apiKey.Name, apiKey.MaskedKey) } if err := pager.Err(); err != nil { panic(err) } apiKey, err := client.APIKeys.Get(ctx, "key_01jwv4tn5m8k3q2v7x9p0a1bc2") if err != nil { panic(err) } fmt.Println(apiKey.ProjectID, apiKey.ExpiresAt) } ``` ## Rename or delete an API key Rename a key when the owner or purpose changes. Delete a key when the workload no longer needs access. An API key cannot delete itself. Authenticate with a different key when deleting a key, so the request cannot revoke the credential that is authorizing it. ```typescript TypeScript theme={null} await kernel.apiKeys.update('key_01jwv4tn5m8k3q2v7x9p0a1bc2', { name: 'staging-ci-rotated', }); await kernel.apiKeys.delete('key_01jwv4tn5m8k3q2v7x9p0a1bc2'); ``` ```python Python theme={null} import os from kernel import Kernel client = Kernel(api_key=os.environ["KERNEL_API_KEY"]) client.api_keys.update( "key_01jwv4tn5m8k3q2v7x9p0a1bc2", name="staging-ci-rotated", ) client.api_keys.delete("key_01jwv4tn5m8k3q2v7x9p0a1bc2") ``` ```go Go theme={null} package main import ( "context" "fmt" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() apiKey, err := client.APIKeys.Update( ctx, "key_01jwv4tn5m8k3q2v7x9p0a1bc2", kernel.APIKeyUpdateParams{ Name: "staging-ci-rotated", }, ) if err != nil { panic(err) } fmt.Println(apiKey.ID, apiKey.Name) if err := client.APIKeys.Delete(ctx, "key_01jwv4tn5m8k3q2v7x9p0a1bc2"); err != nil { panic(err) } } ``` ## Rotate a key `rotate` issues a replacement key in a single call and keeps the old key working for a short grace period, so your workload can switch over without downtime. The new key copies the rotated key's name and project scope, and—like create—Kernel returns the plaintext `key` only once. Two optional parameters control the timing: * `days_to_expire` sets the new key's lifetime in days (`1`-`3650`). Omit it to give the new key the same lifetime the rotated key originally had, or to never expire if the old key never did. * `expire_in_days` sets how long the old key keeps working before it expires. Use `0` to revoke it immediately, or omit it for the default 7-day grace window. The old key stops authenticating automatically once the window passes—you don't need to delete it. ```typescript TypeScript theme={null} const rotated = await kernel.apiKeys.rotate('key_01jwv4tn5m8k3q2v7x9p0a1bc2', { days_to_expire: 30, expire_in_days: 7, }); console.log(rotated.key); // Save this value now. Kernel won't show it again. console.log(rotated.id, rotated.masked_key); ``` ```python Python theme={null} import os from kernel import Kernel client = Kernel(api_key=os.environ["KERNEL_API_KEY"]) rotated = client.api_keys.rotate( "key_01jwv4tn5m8k3q2v7x9p0a1bc2", days_to_expire=30, expire_in_days=7, ) print(rotated.key) # Save this value now. Kernel won't show it again. print(rotated.id, rotated.masked_key) ``` ```go Go theme={null} package main import ( "context" "fmt" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() rotated, err := client.APIKeys.Rotate( ctx, "key_01jwv4tn5m8k3q2v7x9p0a1bc2", kernel.APIKeyRotateParams{ DaysToExpire: kernel.Int(30), ExpireInDays: kernel.Int(7), }, ) if err != nil { panic(err) } fmt.Println(rotated.Key) // Save this value now. Kernel won't show it again. fmt.Println(rotated.ID, rotated.MaskedKey) } ``` After you rotate a key: 1. Store the new plaintext key in your secret manager. 2. Deploy or restart the workload that uses `KERNEL_API_KEY`. 3. Verify the workload can call Kernel before the grace window ends. To cut over immediately instead of using a grace window, pass `expire_in_days: 0` so the old key stops working as soon as the new one is issued. ## Troubleshooting | Error | What it means | What to do | | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------- | | `400 Bad Request` | The name is missing, `days_to_expire` is outside `1`-`3650`, `expire_in_days` is outside `0`-`3650`, or `project_id` is empty. | Send a name, choose a valid expiry, or omit `project_id` for an org-scoped key. | | `400 Bad Request` (rotate) | `days_to_expire` is shorter than `expire_in_days`, so the new key would expire before the old key's grace window ends. | Raise `days_to_expire` or lower `expire_in_days`. | | `401 Unauthorized` | Kernel couldn't authenticate the request. | Set a valid `KERNEL_API_KEY`. | | `404 Not Found` | The project or API key doesn't exist, or the caller can't access it. | Check the ID. If you're using a project-scoped key, you can only rotate keys in that same project. | # Audit Logs Source: https://kernel.sh/docs/info/audit-logs Audit logs for API requests across your organization Audit logs record authenticated API requests across your entire organization. Use them to review who called Kernel, which endpoint they called, when the request happened, and how the request completed. Choose the endpoint that matches the amount of data you need: | Endpoint | Best for | Output | | --------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------ | | [Search](#search-audit-logs) | Interactive investigation and recent activity | Paginated JSON events | | [Download](#download-audit-logs) | Archival, compliance, and offline analysis of a bounded time window | Gzip-compressed JSON Lines (`.jsonl.gz`) | | [Continuous S3 export](#continuous-s3-export) | Ongoing delivery to your data lake or compliance bucket | Partitioned `jsonl.gz` objects in your S3 bucket | Search and download are available on **Start-Up** and **Enterprise** plans. Continuous S3 export is available on **Enterprise**. Audit logs are also available from the [CLI](/docs/reference/cli/audit-logs). Kernel retains audit log events for 1 year. Search and download can access any event within this window. ## Filter audit logs The API and SDKs use the same filters for search and downloads: * `auth_strategy` filters by authentication method, such as `api_key`, `dashboard`, or `oauth`. * `service` filters by the service that emitted the audit event. * `method` returns only requests that use the specified HTTP method. * `exclude_method` omits requests that use any of the specified HTTP methods. * `search` matches path, user ID, email, client IP, or status. * `search_user_id` matches requests from the specified user IDs in addition to any free-text matches. ## Search audit logs Each API page contains up to 100 events. The SDK pagination helpers request older pages as you iterate. ```typescript TypeScript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel({ apiKey: process.env.KERNEL_API_KEY, }); for await (const event of kernel.auditLogs.list({ start: '2026-06-01T00:00:00Z', end: '2026-06-02T00:00:00Z', method: 'POST', })) { console.log(event.timestamp, event.method, event.path, event.status); } ``` ```python Python theme={null} import os from kernel import Kernel client = Kernel(api_key=os.environ["KERNEL_API_KEY"]) for event in client.audit_logs.list( start="2026-06-01T00:00:00Z", end="2026-06-02T00:00:00Z", method="POST", ): print(event.timestamp, event.method, event.path, event.status) ``` ```go Go theme={null} package main import ( "context" "fmt" "time" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() pager := client.AuditLogs.ListAutoPaging(ctx, kernel.AuditLogListParams{ Start: time.Date(2026, time.June, 1, 0, 0, 0, 0, time.UTC), End: time.Date(2026, time.June, 2, 0, 0, 0, 0, time.UTC), Method: kernel.String("POST"), }) for pager.Next() { event := pager.Current() fmt.Println(event.Timestamp, event.Method, event.Path, event.Status) } if err := pager.Err(); err != nil { panic(err) } } ``` See the [API reference](https://kernel.sh/docs/api-reference/audit-logs/list-audit-logs) for the full request and response schema. ## Download audit logs A download covers a time window of up to 30 days. The SDK download helpers default to `jsonl.gz` and write the result to a destination you provide. The helpers don't close the destination. Python provides equivalent sync and async methods; both accept a synchronous binary destination. ```typescript TypeScript theme={null} import { open } from 'node:fs/promises'; import Kernel from '@onkernel/sdk'; const kernel = new Kernel({ apiKey: process.env.KERNEL_API_KEY, }); const file = await open('audit-logs.jsonl.gz', 'w'); try { await kernel.auditLogs.download( { start: '2026-06-01T00:00:00Z', end: '2026-06-02T00:00:00Z', exclude_method: ['GET'], }, file, ); } finally { await file.close(); } ``` ```python Python theme={null} import os from kernel import Kernel client = Kernel(api_key=os.environ["KERNEL_API_KEY"]) with open("audit-logs.jsonl.gz", "wb") as file: client.audit_logs.download( to=file, start="2026-06-01T00:00:00Z", end="2026-06-02T00:00:00Z", exclude_method=["GET"], ) ``` ```go Go theme={null} package main import ( "context" "os" "time" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() file, err := os.Create("audit-logs.jsonl.gz") if err != nil { panic(err) } defer file.Close() _, err = client.AuditLogs.Download(ctx, kernel.AuditLogDownloadParams{ Start: time.Date(2026, time.June, 1, 0, 0, 0, 0, time.UTC), End: time.Date(2026, time.June, 2, 0, 0, 0, 0, time.UTC), ExcludeMethod: []string{"GET"}, }, file) if err != nil { panic(err) } } ``` Downloaded chunks contain one JSON object per line. They use the same fields as search results and add `event_id`. For direct HTTP integrations, see the [API reference](https://kernel.sh/docs/api-reference/audit-logs/download-an-audit-log-export-chunk) for pagination headers, formats, and the full request and response schema. ## Continuous S3 export Continuous export writes new audit log events to an S3 bucket that you control. A destination is an organization-level resource: Kernel assumes an IAM role in your AWS account and writes `jsonl.gz` objects to the configured bucket. The destination is created paused so you can configure and verify IAM before delivery starts. ### Set up a destination You can use the SDKs, the [CLI](/docs/reference/cli/audit-logs#kernel-audit-logs-export), or the HTTP API. All destination requests require an organization-level credential. The setup steps below use the SDKs. For the CLI walkthrough, see the [CLI reference](/docs/reference/cli/audit-logs#kernel-audit-logs-export). #### 1. Create a paused destination Create the destination with the customer role ARN. The create response contains the destination `id`, the Kernel role ARN that must be trusted (`kernel_role_arn`), and the unique STS external ID (`external_id`). Save all three values. The `external_id` is not your organization ID and is not interchangeable with an external ID from another destination. Every destination is an S3 destination in `jsonl.gz` format and starts with `status: "paused"`. If you use KMS, set `kms_key_id` to a key ID, alias, or ARN in the destination region. ```typescript TypeScript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel({ apiKey: process.env.KERNEL_API_KEY, }); const destination = await kernel.auditLogs.exportDestinations.create({ type: 's3', format: 'jsonl.gz', region: 'us-east-1', bucket: 'customer-audit-logs', prefix: 'audit-logs', role_arn: 'arn:aws:iam::123456789012:role/customer-audit-log-export', // kms_key_id: 'arn:aws:kms:us-east-1:123456789012:key/11111111-2222-3333-4444-555555555555', }); console.log(destination.id); console.log(destination.kernel_role_arn); console.log(destination.external_id); ``` ```python Python theme={null} import os from kernel import Kernel client = Kernel(api_key=os.environ["KERNEL_API_KEY"]) destination = client.audit_logs.export_destinations.create( type="s3", format="jsonl.gz", region="us-east-1", bucket="customer-audit-logs", prefix="audit-logs", role_arn="arn:aws:iam::123456789012:role/customer-audit-log-export", # kms_key_id="arn:aws:kms:us-east-1:123456789012:key/11111111-2222-3333-4444-555555555555", ) print(destination.id) print(destination.kernel_role_arn) print(destination.external_id) ``` ```go Go theme={null} package main import ( "context" "fmt" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() destination, err := client.AuditLogs.ExportDestinations.New(ctx, kernel.AuditLogExportDestinationNewParams{ CreateAuditLogExportDestinationRequest: kernel.CreateAuditLogExportDestinationRequestParam{ Type: kernel.CreateAuditLogExportDestinationRequestTypeS3, Format: kernel.CreateAuditLogExportDestinationRequestFormatJSONLGz, Region: "us-east-1", Bucket: "customer-audit-logs", Prefix: "audit-logs", RoleArn: "arn:aws:iam::123456789012:role/customer-audit-log-export", // KmsKeyID: kernel.String("arn:aws:kms:us-east-1:123456789012:key/11111111-2222-3333-4444-555555555555"), }, }) if err != nil { panic(err) } fmt.Println(destination.ID) fmt.Println(destination.KernelRoleArn) fmt.Println(destination.ExternalID) } ``` #### 2. Configure the IAM trust policy Update the trust policy on the customer role supplied as `role_arn`. Use the `kernel_role_arn` and `external_id` returned by the create call. The values below are placeholders; replace both of them with the values from your response. ```json theme={null} { "Version": "2012-10-17", "Statement": [ { "Sid": "AllowKernelAuditLogExport", "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::123456789012:role/kernel-audit-log-export" }, "Action": "sts:AssumeRole", "Condition": { "StringEquals": { "sts:ExternalId": "external-id-from-create-response" } } } ] } ``` `Principal.AWS` must be the returned `kernel_role_arn`, not the customer role's own ARN. The `sts:ExternalId` condition must be the returned `external_id`. Recreating a destination generates a new external ID, so update this trust policy again if you recreate it. #### 3. Grant S3 and KMS permissions Attach an identity policy to the customer role. Grant `s3:PutObject` for real delivery and the test probe. Granting `s3:DeleteObject` is recommended so Kernel can remove the temporary probe after a successful test. The resource should match the configured prefix. If you use a different prefix, replace `audit-logs/*` below. Omit the KMS statement when `kms_key_id` is empty. ```json theme={null} { "Version": "2012-10-17", "Statement": [ { "Sid": "WriteAuditLogObjects", "Effect": "Allow", "Action": [ "s3:PutObject", "s3:DeleteObject" ], "Resource": "arn:aws:s3:::customer-audit-logs/audit-logs/*" }, { "Sid": "UseAuditLogKMSKey", "Effect": "Allow", "Action": "kms:GenerateDataKey", "Resource": "arn:aws:kms:us-east-1:123456789012:key/11111111-2222-3333-4444-555555555555" } ] } ``` When KMS is configured, the KMS key policy must also allow the customer role to use `kms:GenerateDataKey` (unless the key policy delegates access to IAM policies). The test and real uploads use the same encryption request: SSE-KMS with the configured key. Without a configured KMS key, the bucket's default encryption applies. #### 4. Test and activate The test endpoint assumes the customer role, writes a temporary gzip probe, and attempts to delete it. The probe uses the same request metadata as a real delivery: a SHA-256 checksum, `Content-Type: application/gzip`, and SSE-KMS when configured. Run the test before changing the destination to active. A successful test returns `stage: "complete"`. A failed test identifies `assume_role` or `put_object` and returns `assume_role_failed` or `put_object_failed`. Do not activate the destination until the test succeeds. ```typescript TypeScript theme={null} const test = await kernel.auditLogs.exportDestinations.test(destination.id); if (!test.success) { throw new Error(`export test failed at ${test.stage}`); } const active = await kernel.auditLogs.exportDestinations.update(destination.id, { status: 'active', }); console.log(active.status); // active ``` ```python Python theme={null} test = client.audit_logs.export_destinations.test(destination.id) if not test.success: raise RuntimeError(f"export test failed at {test.stage}") active = client.audit_logs.export_destinations.update( destination.id, status="active", ) print(active.status) # active ``` ```go Go theme={null} test, err := client.AuditLogs.ExportDestinations.Test(ctx, destination.ID) if err != nil { panic(err) } if !test.Success { panic(fmt.Sprintf("export test failed at %s", test.Stage)) } active, err := client.AuditLogs.ExportDestinations.Update(ctx, destination.ID, kernel.AuditLogExportDestinationUpdateParams{ UpdateAuditLogExportDestinationRequest: kernel.UpdateAuditLogExportDestinationRequestParam{ Status: kernel.UpdateAuditLogExportDestinationRequestStatusActive, }, }) if err != nil { panic(err) } fmt.Println(active.Status) // active ``` Activation starts delivery at the activation time. It does not backfill events recorded before activation. ### Object layout Every continuous export object uses this exact key layout: ``` /destination_id=/org_id=/date=/hour=/-.jsonl.gz ``` For example, a destination with prefix `audit-logs` writes under `audit-logs/destination_id=...`. If the prefix is empty, the key starts with `destination_id=...` and has no leading slash. The `date` and `hour` partitions are UTC and identify the calendar hour that fully contains every row in the object. This makes the layout safe for Hive-style partitioning. The format is always `jsonl.gz`: each decompressed line is one JSON event, including `event_id`. ### Delivery semantics * Delivery is **at least once**. A retry can rewrite the same object. * Each event-time window is held for about 10 minutes before it commits. A row that becomes visible after its window has committed may not be delivered. * Delivery begins at activation time; events before activation are not backfilled. * Pausing stops new delivery attempts. Resuming starts from the resume time; events recorded while paused are never exported. Pausing is not a way to defer delivery. * An S3 upload already in progress may complete after a pause or delete. Its rows can appear again after the destination is resumed. ### Monitor delivery Use the SDK retrieve and list methods, the [CLI](/docs/reference/cli/audit-logs#kernel-audit-logs-export), or the HTTP GET endpoints to inspect the destination and its delivery health. The health fields are: | Field | Meaning | | ---------------------- | ----------------------------------------------------------------------------------------------- | | `status` | `active` or `paused`. | | `last_exported_cursor` | Opaque forward-only checkpoint for continuous delivery. It is not an audit-log list page token. | | `last_success_at` | Time of the most recent successful export upload. | | `last_error` | Sanitized description of the most recent delivery failure. | | `last_error_at` | Time of the most recent delivery failure. | | `consecutive_failures` | Number of consecutive failed delivery attempts. | | `next_attempt_at` | Scheduled time for the next delivery attempt after a failure. | The cursor and timestamp/error fields can be absent until the destination has attempted delivery. Use `last_error`, `last_error_at`, and `consecutive_failures` together when investigating a destination that is active but not progressing. ### Troubleshoot delivery * **`assume_role` / `assume_role_failed`:** Check the customer role's trust policy. The principal must be the current `kernel_role_arn` from the destination response, and `sts:ExternalId` must equal the current `external_id`. Recreating a destination issues a new external ID; update the trust policy after recreation. * **`put_object` / `put_object_failed`:** Confirm that the customer role can write to the configured bucket and prefix with `s3:PutObject`, and that the bucket is in the configured commercial AWS region. Grant `s3:DeleteObject` as well so the test probe can be cleaned up. * **KMS failures:** Confirm that the KMS key is in the commercial `aws` partition, that an ARN's region matches the destination region, and that both the role policy and key policy allow `kms:GenerateDataKey`. If you do not need a customer-managed key, clear it and use bucket-default encryption. * **A probe remains after a successful test:** Probe cleanup is best effort. Add `s3:DeleteObject` for the configured prefix and remove any leftover `.kernel-audit-log-export-test-*.jsonl.gz` object yourself. * **A `PATCH` returns `409 Conflict`:** The destination changed concurrently. Retrieve fresh state, merge your intended fields with that state, and retry the update. Do not retry a stale read-modify-write payload unchanged. * **No objects after a pause or downgrade:** Check `status` and the plan. Pausing and non-Enterprise status stop new delivery; restore Enterprise and follow the normal activation/resume path if the destination is paused. Events recorded while paused are not backfilled. # Concepts Source: https://kernel.sh/docs/info/concepts ## Browser A `Browser` is a cloud-based browser managed by Kernel. They accept Chrome DevTools Protocol connections and can be used to run browser automations or web agents. ## Browser Pool A `Browser Pool` is a set of identically-configured browsers that Kernel keeps ready for immediate use. You acquire a browser from the pool when a task starts and release it back when the task finishes. Browser pools remove browser start-up latency from your workload — see [Browser Pools](/docs/browsers/pools). ## App An `App` is a codebase deployed on Kernel. You can use Kernel for a variety of use cases, including web automations, data processing, and more. ## Action An `Action` is an invokable method within an app. Actions allow your to register entry points or functions that can be triggered on-demand. Actions can call non-action methods. Apps can have multiple actions. ## Invocation An `Invocation` is a single execution of an action. Invocations can be triggered via API, scheduled as a job, or run on-demand. # Pricing & Limits Source: https://kernel.sh/docs/info/pricing With Kernel, you only pay for what you use and nothing more. You don't pay for idle time thanks to [Standby Mode](/docs/browsers/standby), idle browsers in a browser pool incur no usage charges, and you're never charged for proxies. ## Plan Pricing | Plan | Monthly cost | Included Credits / mo | | ---------- | ------------- | --------------------- | | Developer | Free + usage | \$5 | | Hobbyist | \$30 + usage | \$10 | | Start-Up | \$200 + usage | \$50 | | Enterprise | Custom | Custom | ## Usage Rates | Usage type | Price (\$/sec) | | ---------------------------------- | -------------- | | Headful browser | 0.0001333336 | | Headless browser | 0.0000166667 | | Headful browser + GPU acceleration | 0.0008000016 | | App invocation | 0.0000666668 | > Included monthly credits apply to usage costs only. ### Pricing calculator ## Managed infrastructure | Feature | Developer | Hobbyist | Start-Up | Enterprise | | --------------------------------- | --------- | --------- | --------- | ------------ | | Browser live view | ✅ | ✅ | ✅ | ✅ | | Extended browser timeouts | ✅ | ✅ | ✅ | ✅ | | Browser live view | ✅ | ✅ | ✅ | ✅ | | Configurable browser viewports | ✅ | ✅ | ✅ | ✅ | | Managed stealth mode | ✅ | ✅ | ✅ | ✅ | | Computer controls API | ✅ | ✅ | ✅ | ✅ | | Browser pools | ✅ | ✅ | ✅ | ✅ | | SOC2 compliance | ✅ | ✅ | ✅ | ✅ | | Browser replays | 1 day | 7 days | 30 days | Custom | | Managed auth connections | 3 | unlimited | unlimited | unlimited | | Vaults | 3 | unlimited | unlimited | unlimited | | Custom browser extensions | 1 | 1 | unlimited | unlimited | | Projects | 1 | 1 | unlimited | unlimited | | Support | Discord | Email | Email | Shared Slack | | Configurable & BYO proxies | ❌ | ❌ | ✅ | ✅ | | GPU acceleration | ❌ | ❌ | ✅ | ✅ | | Audit log search & export | ❌ | ❌ | ✅ | ✅ | | Continuous audit log export to S3 | ❌ | ❌ | ❌ | ✅ | | HIPAA compliance (BAA) | ❌ | ❌ | ❌ | ✅ | ## Concurrency limits Kernel enforces a single concurrency limit covering all browsers you run at once—whether created on demand with `browsers.create()` or reserved in a [browser pool](/docs/browsers/pools/overview). Your full limit is available to either API in any mix. | Feature | Developer | Hobbyist | Start-Up | Enterprise | | ---------------------------------- | --------------- | -------------- | ------------------ | ---------- | | Concurrent browsers | 5 | 10 | 150 | Custom | | App invocations | 5 | 10 | 50 | Custom | | App invocations (per-app) | 5 | 10 | 20 | Custom | | Managed auth health check interval | 6 hours minimum | 1 hour minimum | 20 minutes minimum | Custom | #### Notes * Reserved capacity in a [browser pool](/docs/browsers/pools/overview) counts toward your concurrency limit whether or not the browsers are currently acquired—a pool sized to 40 browsers uses 40 of your limit. * Browsers in [Standby Mode](/docs/browsers/standby) count against your concurrency limit. * Limits are org-wide by default unless stated otherwise. ## Rate limiting Kernel enforces per-organization rate limits on API requests. When you exceed the rate limit, the API returns a `429 Too Many Requests` response with a `Retry-After` header indicating how many seconds to wait before retrying. Rate-limited endpoints include these headers on every response: | Header | Description | | ----------------------- | --------------------------------------------------------- | | `X-RateLimit-Limit` | Maximum requests allowed per minute | | `X-RateLimit-Remaining` | Requests remaining in the current window | | `Retry-After` | Seconds to wait before retrying (only on `429` responses) | All Kernel SDKs automatically retry `429` responses up to 2 times, respecting the `Retry-After` header for delay timing. If retries are exhausted, the SDK throws a typed `RateLimitError` with the response headers accessible for custom backoff logic. If you need higher rate limits, [contact us](https://calendly.com/d/d3tn-5kp-5yt). ## FAQ App invocations are billed for active compute time, not per API call. The invocation rate in [Usage Rates](#usage-rates) is based on the current 4 GB memory allocation at \$0.0000166667 per GB-second. Billing starts when your code begins executing and stops when it finishes. You aren't charged for queued time, deploying an app, or leaving a deployed app idle. Failed and canceled invocations still accrue charges for the time they ran. Browsers created by an invocation are billed separately for their active runtime at the browser rates above. Services your code calls, such as an LLM API, also bill you independently. The [app invocation limits](#concurrency-limits) are concurrency limits, not a number of invocations included with your plan. see this guide on [spending controls](/docs/info/spending-caps). Only for active runtime. `timeout_seconds` sets an idle auto-delete ceiling, not a billing window. Once a browser goes idle — 5 seconds after the last CDP or Live View activity — it enters Standby Mode and stops accruing usage cost, even if it stays alive until the timeout is reached. Deleting a browser early doesn't lower cost any further (idle time is already free), but it does free up your concurrency slot sooner. you pay the standard usage-based price per GB-second while browsers are running. Idle browsers in a pool incur no disk charges—you only pay when a browser is actively in use. Note: A browser pool counts toward your concurrency limit whether or not its browsers are currently acquired — a browser pool sized to 40 browsers uses 40 of your limit. Browser pools are available on Start-Up and Enterprise plans. Managed Auth is included on all plans with no per-connection fees. It uses browser sessions for login, health checks, and eligible automatic reauthentication. These count toward your browser usage and concurrency like any other browser session. Auth sessions are fast, typically 5-30 seconds each, and most website sessions remain valid for days. For example, monitoring 100 auth connections typically costs less than \$5/month in browser usage. Free users can create up to three vaults. Unlimited vaults are included in paid plans. There are no surcharges to use Kernel's agentic payments products. # Projects Source: https://kernel.sh/docs/info/projects Organize resources and isolate access within your Kernel organization A **Project** is a named container for Kernel resources inside an organization. Use projects to separate environments (like `production` and `staging`), split resources between teams, or isolate customer workloads — each project has its own browsers, profiles, credentials, proxies, extensions, deployments, and browser pools. ## Why Projects? * **Isolate environments** — keep `production` resources apart from `staging` or experiments. * **Scope access** — issue API keys that can only see resources in one project. * **Concurrency limits** — set an org-wide default cap for every project, or override it per project, so one team or environment can't exhaust your org quota. ## The Default Project Every organization has at least one project. Resources that existed before projects were introduced have been moved into a project named **Default**, so your existing browsers, apps, profiles, and other resources continue to work without any changes on your end. Your organization must always have **at least one active project**. The API returns `409 Conflict` if you try to delete the last remaining project: ```json theme={null} { "code": "last_active_project", "message": "organization must have at least one project" } ``` A project must also be empty before it can be deleted. If active resources remain, the API returns `409 Conflict` with code `project_not_empty`; delete or otherwise remove those resources and retry. Organizations without Projects enabled receive `404 Not Found` with code `projects_disabled` from project-management endpoints. ## Scoping Requests to a Project Pass the `X-Kernel-Project-Id` header with a project ID on any API request to scope it to a specific project. Project names are not accepted in this header. Without the header (and without a project-scoped API key), requests act on your organization's **default project**: reads return the default project's resources, and writes create resources in it. ```bash theme={null} curl https://api.onkernel.com/browsers \ -H "Authorization: Bearer $KERNEL_API_KEY" \ -H "X-Kernel-Project-Id: proj_abc123" ``` ### Resolution rules * **Unknown or archived project** — a header naming a project that doesn't exist or isn't active fails with `404 Not Found`: ```json theme={null} { "code": "project_not_found", "message": "Project not found or inactive" } ``` * **The default project is the baseline** — naming your organization's default project is exactly equivalent to omitting the header, for both reads and writes. * **Scoped keys reject mismatches** — with a project-scoped API key, a header naming any other project fails with `403 Forbidden` (see [API keys](#api-keys) below). ### SDK usage Set the project ID on the client so every request is scoped to that project. ```typescript TypeScript theme={null} import Kernel from '@onkernel/sdk'; // Scope the whole client to a project const kernel = new Kernel({ projectID: 'proj_abc123', }); const browser = await kernel.browsers.create(); ``` ```python Python theme={null} from kernel import Kernel # Scope the whole client to a project kernel = Kernel( project_id="proj_abc123", ) browser = kernel.browsers.create() ``` ```go Go theme={null} package main import ( "context" "github.com/kernel/kernel-go-sdk" "github.com/kernel/kernel-go-sdk/option" ) func main() { ctx := context.Background() // Scope the whole client to a project. client := kernel.NewClient( option.WithProjectID("proj_abc123"), ) browser, err := client.Browsers.New(ctx, kernel.BrowserNewParams{}) if err != nil { panic(err) } _ = browser } ``` ## Authentication and Project Scope ### API keys API keys can be **org-wide** or **project-scoped**. * **Existing API keys are org-wide.** They see every resource in your organization across all projects. Include an `X-Kernel-Project-Id` header to restrict a single request to one project. * **Project-scoped API keys** can only access resources inside the project they were issued for. Create one from the **API Keys** page in the dashboard, the [CLI](/docs/reference/cli/api-keys), an SDK, or the [API keys guide](/docs/info/api-keys), and pass the target `project_id` when generating the key. Requests made with a scoped key are automatically limited to that project — no header required. If you do send an `X-Kernel-Project-Id` header and it conflicts with the key's project, the request is rejected with `403 Forbidden`. * **Most project administration requires an org-wide credential.** A project-scoped key may rename its own project, but cannot create, archive, or delete projects, or change project limits. ### OAuth OAuth tokens (used by the Kernel CLI and MCP server) are **always org-wide**. You cannot bind an OAuth session to a single project. To scope OAuth-authenticated requests, send the `X-Kernel-Project-Id` header with each request — or use the CLI's `--project` flag (see below). ## Using Projects from the CLI The Kernel [CLI](/docs/reference/cli/projects) has first-class project support: * A global `--project ` flag scopes any command to a single project ID. * The `KERNEL_PROJECT` environment variable does the same, so you can set it once in your shell or CI. * A `kernel projects` command group lets you list, create, get, and delete projects, and manage per-project limit overrides. ```bash theme={null} # Scope a single command kernel browsers list --project proj_abc123 # Scope every command in the shell export KERNEL_PROJECT=proj_abc123 kernel apps list # Manage projects kernel projects list kernel projects create staging kernel projects limits set staging --max-concurrent-sessions 5 ``` Under the hood, `--project` (or the env var) adds the `X-Kernel-Project-Id` header to every authenticated request. It's the recommended way to target a specific project when you're logged in with OAuth (`kernel login`), since OAuth itself is always org-wide. ## Managing Projects Use the `/org/projects` REST endpoints (or the SDKs' `projects` resource) to manage projects. | Method | Path | Description | | -------- | -------------------- | ---------------------------------------------------------------- | | `GET` | `/org/projects` | List projects in the organization | | `POST` | `/org/projects` | Create a project | | `GET` | `/org/projects/{id}` | Get a project by ID or name | | `PATCH` | `/org/projects/{id}` | Update a project's name or status (`active` / `archived`) | | `DELETE` | `/org/projects/{id}` | Delete a project (must be empty and not the last active project) | ### Create a project ```typescript TypeScript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); const project = await kernel.projects.create({ name: 'staging' }); console.log(project.id); // proj_abc123 ``` ```python Python theme={null} from kernel import Kernel kernel = Kernel() project = kernel.projects.create(name="staging") print(project.id) # proj_abc123 ``` ```go Go theme={null} package main import ( "context" "fmt" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() project, err := client.Projects.New(ctx, kernel.ProjectNewParams{ CreateProjectRequest: kernel.CreateProjectRequestParam{ Name: "staging", }, }) if err != nil { panic(err) } fmt.Println(project.ID) // proj_abc123 } ``` ### List projects ```typescript TypeScript theme={null} for await (const project of kernel.projects.list()) { console.log(project.id, project.name, project.status); } ``` ```python Python theme={null} for project in kernel.projects.list(): print(project.id, project.name, project.status) ``` ```go Go theme={null} pager := client.Projects.ListAutoPaging(ctx, kernel.ProjectListParams{}) for pager.Next() { project := pager.Current() fmt.Println(project.ID, project.Name, project.Status) } if err := pager.Err(); err != nil { panic(err) } ``` ### Get a project Project names are unique within an organization. You can retrieve a project by its ID or by its name. ```typescript TypeScript theme={null} const project = await kernel.projects.get('staging'); console.log(project.id); // proj_abc123 ``` ```python Python theme={null} project = kernel.projects.get("staging") print(project.id) # proj_abc123 ``` ```go Go theme={null} project, err := client.Projects.Get(ctx, "staging") if err != nil { panic(err) } fmt.Println(project.ID) // proj_abc123 ``` ### Update a project ```typescript TypeScript theme={null} await kernel.projects.update('proj_abc123', { name: 'production' }); ``` ```python Python theme={null} kernel.projects.update("proj_abc123", name="production") ``` ```go Go theme={null} project, err := client.Projects.Update( ctx, "proj_abc123", kernel.ProjectUpdateParams{ UpdateProjectRequest: kernel.UpdateProjectRequestParam{ Name: kernel.String("production"), }, }, ) if err != nil { panic(err) } _ = project ``` ### Delete a project ```typescript TypeScript theme={null} await kernel.projects.delete('proj_abc123'); ``` ```python Python theme={null} kernel.projects.delete("proj_abc123") ``` ```go Go theme={null} if err := client.Projects.Delete(ctx, "proj_abc123"); err != nil { panic(err) } ``` Project deletion is a soft delete. A project that still owns active resources returns `project_not_empty`; the final active project returns `last_active_project`. ## Concurrency Limits Kernel caps how many browsers can run at once, at two levels. A single limit covers both on-demand browsers (`browsers.create()`) and [browser pools](/docs/browsers/pools) — standalone sessions and pool capacity count against the same cap. * **Organization limit** — the total concurrent browsers allowed across your whole organization, determined by your plan. Every browser session and every browser in a browser pool counts against it. * **Per-project limits** — optional caps on individual projects, so one team or environment can't consume the entire org limit. Per-project caps come from two places: * **An org-wide default** — one value that every project inherits unless it sets its own. It applies to existing and newly created projects alike, so you don't have to configure each project by hand. * **A per-project override** — an explicit cap on a single project that takes precedence over the default. A project's effective cap resolves in this order: 1. The project's explicit override, if set. 2. Otherwise, the organization's default project cap, if set. 3. Otherwise, no per-project cap — only the organization limit applies. A per-project cap never lets a project exceed your organization's concurrency limit. Concurrency limits control how many browsers can run at once. To control monthly usage cost instead, see [Spending Caps](/docs/info/spending-caps). | Method | Path | Description | | ------- | --------------------------- | ---------------------------------------------------------------- | | `GET` | `/org/limits` | Get the org concurrency limit and the default per-project cap | | `PATCH` | `/org/limits` | Set the default per-project cap (send `0` to clear it) | | `GET` | `/org/projects/{id}/limits` | Get a single project's limit overrides | | `PATCH` | `/org/projects/{id}/limits` | Set a single project's limit overrides (send `0` to clear a cap) | `max_pooled_sessions` is deprecated. Pooled browsers now count toward `max_concurrent_sessions`, and requests that set `max_pooled_sessions` return a `400`. ### Set an org-wide default Apply a default of 10 concurrent browsers to every project that doesn't have its own override: ```bash cURL theme={null} curl -X PATCH https://api.onkernel.com/org/limits \ -H "Authorization: Bearer $KERNEL_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "default_project_max_concurrent_sessions": 10 }' ``` ```typescript TypeScript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); await kernel.organization.limits.update({ default_project_max_concurrent_sessions: 10, }); ``` ```python Python theme={null} from kernel import Kernel kernel = Kernel() kernel.organization.limits.update(default_project_max_concurrent_sessions=10) ``` ```go Go theme={null} package main import ( "context" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() _, err := client.Organization.Limits.Update(ctx, kernel.OrganizationLimitUpdateParams{ UpdateOrgLimitsRequest: kernel.UpdateOrgLimitsRequestParam{ DefaultProjectMaxConcurrentSessions: kernel.Int(10), }, }) if err != nil { panic(err) } } ``` To cap a specific project differently, set an explicit override on it with `PATCH /org/projects/{id}/limits` — that value takes precedence over the default. See the [API reference](https://kernel.sh/docs/api-reference/projects/list-projects) for full request and response schemas, including `ProjectLimits` for per-project concurrency caps. # Spending Caps Source: https://kernel.sh/docs/info/spending-caps Choose a monthly spending guardrail for your organization, a project, or both A retry loop, traffic spike, or long-running agent can raise usage faster than you expect. Spending caps warn you before that usage reaches the amount you choose, then pause new work for the affected scope. If you only set one cap, start with an organization cap. It protects the total bill. Add project caps when a team, environment, customer, or experiment needs a tighter boundary. ## Which cap should you use? Use both for a multi-project production setup: one organization cap for the total bill, plus lower project caps where a single workload needs tighter control. Subscription fees don't count toward either cap. Spending caps apply to usage charges. ## Set a cap Go to the [KERNEL billing page](https://dashboard.onkernel.com/billing) as an organization admin. In **Spending Cap**, select **Organization** or **Project**. For a project cap, select the project you want to control. Enter the usage amount in usd and save. The card shows month-to-date progress against the limit. Availability varies by scope: * **Organization caps:** Available to eligible self-serve organizations. Enterprise access is in limited preview. * **Project caps:** In limited preview for all organizations. [Email support@kernel.sh](mailto:support@kernel.sh?subject=Spending%20cap%20preview%20access) to request preview access. Set an organization spending cap or manage the project caps available to your organization. ## What happens during the month KERNEL evaluates each cap from the first day of the month at 00:00 utc. Organization and project usage accumulate independently. KERNEL sends a warning email. Existing and new work continue. New browser sessions and app invocations return a `403` for the affected scope. Running work continues until it ends. New work resumes when the next monthly period starts. An organization admin can also increase or remove the cap at any time. A spending cap is an admission guardrail, not a transactional hard ceiling. Billing events take time to evaluate, and already-running work can continue adding usage after the threshold is reached. Leave headroom below any budget that must not be exceeded. ## How spend is counted An organization cap measures usage after KERNEL applies your included monthly credits: ```text theme={null} out-of-pocket usage = gross usage - included monthly credits ``` For example, an organization with \$50 in monthly usage credits sets a \$100 cap: * KERNEL sends the 80% warning at \$130 in gross usage: \$50 in credits plus \$80 in out-of-pocket usage. * KERNEL pauses new usage at \$150 in gross usage: \$50 in credits plus the \$100 cap. * The plan's subscription fee remains separate. Choose this cap when your main requirement is a predictable maximum for monthly usage charges, regardless of which project creates the spend. A project cap measures that project's gross usage. Organization credits aren't divided or assigned to individual projects. For example, a \$25 project cap warns at \$20 and pauses new usage for that project at \$25. Other projects continue until they reach their own caps or the organization cap. Requests without an explicit project resolve to your [default project](/docs/info/projects#the-default-project) and count toward that project's cap. Choose this cap when one workload must not consume an open-ended share of the organization's budget. ## Layer organization and project caps Organization and project caps are independent. KERNEL doesn't allocate the organization cap among projects, and project caps don't have to add up to it. KERNEL pauses new work for that project. Other projects continue. KERNEL pauses new work across every project, even if individual project caps have room left. The organization cap takes precedence because it applies to the full organization. If a project cap is higher than the organization cap plus monthly credits, the organization cap can trigger first. The dashboard warns you about this configuration but still lets you save it because later changes to credits or either cap can change which threshold comes first. ## Common setups | Your setup | Recommended caps | | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | One production workload | Start with an organization cap above normal monthly usage. | | Production plus staging or experiments | Use an organization cap, then lower project caps for non-production work. | | Multiple teams | Use an organization cap, then project caps to isolate each team's usage. | | Customer-specific workloads | Put each customer in a [project](/docs/info/projects), cap each project, and keep an organization cap as the final boundary. | ## Operational details Already-running browser sessions and app invocations continue until they end. Read and delete requests remain available, along with selected non-billable administrative operations such as managing projects, limits, and api keys. Browser pools can maintain idle capacity, but you can't acquire a new browser for a capped scope. The scope can enter the reached state as soon as billing data finishes evaluating. Raise or remove the cap if you need to resume new work before the next monthly period. Organization members can view cap status. Only organization admins can set, edit, or remove caps. Spending caps bound monthly usage cost. [Concurrency limits](/docs/info/pricing#concurrency-limits) bound simultaneous browser capacity. A spending cap doesn't reserve throughput, and a concurrency limit doesn't bound monthly spend. # Support Source: https://kernel.sh/docs/info/support ## Trial, Free, Hobbyist Users on the Trial, Free, and Hobbyist plans get email support on a best-effort basis. Check out our [Discord](https://discord.gg/FBrveQRcud) to get support from our community. ## Start-up Users on the Start-up plan get priority email support. ## Enterprise support tiers Kernel offers tiered enterprise support with defined response times, dedicated channels, and account management. All enterprise tiers include everything in the base [Enterprise plan](/docs/info/pricing). ### Standard * Private Slack channel * Availability: Priority Mon-Fri, 9-5 * Response time: Priority 1 business day, Normal 2 business days * Monthly usage reviews ### Premium Everything in Standard, plus: * Response time: Priority 4 hours, Normal 2 business days * Dedicated account manager * Quarterly business reviews ### Platinum Everything in Premium, plus: * Availability: Priority 24/7, Normal Mon-Fri, 9-5 * Response time: Priority 1 hour, Normal 1 business day * Custom SLA agreements * Priority incident escalation ### Platinum+ Everything in Platinum, plus: * Response time: Priority 15 minutes, Normal 1 business day * Custom compliance and security reviews ## Priority levels | Level | Description | | -------- | ----------------------------------------------- | | Priority | Production blocked or impacted | | Normal | Product bugs, questions, feature requests, etc. | ## Response times | Level | Standard | Premium | Platinum | Platinum+ | | -------- | ------------------------------ | ------------------------------ | ----------------------------- | ----------------------------- | | Priority | 1 business day (Mon-Fri, 9-5) | 4 hours (Mon-Fri, 9-5) | 1 hour (24/7) | 15 minutes (24/7) | | Normal | 2 business days (Mon-Fri, 9-5) | 2 business days (Mon-Fri, 9-5) | 1 business day (Mon-Fri, 9-5) | 1 business day (Mon-Fri, 9-5) | ## Support channels All enterprise tiers include a Private Slack channel for direct communication with the Kernel team. To discuss enterprise plans, book a demo [here](https://calendly.com/d/d3tn-5kp-5yt). # Browsers on Unikernels Source: https://kernel.sh/docs/info/unikernels ## Introduction At Kernel, we've [pioneered](https://news.ycombinator.com/item?id=43705144) a revolutionary approach to browser automation infrastructure: running Chromium browsers on lightweight unikernels. This architecture represents a significant departure from traditional solutions (warm pools / Kubernetes-orchestrated Docker containers), offering unique capabilities that set our platform apart. ## Technical architecture Our platform consists of two key components: 1. **Your app code**: we host your app code on isolated unikernel instances, providing isolation and security. 2. **Browser runtime**: each app instance gets its own Chromium browser running on a dedicated Unikraft-based unikernel. Your app code connects to and runs alongside the browser in the cloud. When you use our [app code](/docs/apps/deploy) platform, we co-locate your browser automations scripts with the browser environment. This solves a number of issues that remote browsers have, including latency, errors due to unexpected disconnects, and bandwidth issues during data-intensive operations like screenshots. ## Unikernels and browsers ### Unikernels Unikernels represent a fundamentally different approach to operating system design. Unlike traditional operating systems that include drivers, services, and components for general-purpose computing, unikernels are specialized, single-purpose systems that include only the necessary components to run a specific application. ### Browsers Browsers are inherently designed to run untrusted code from the internet. To handle this safely, browser architectures incorporate sophisticated isolation mechanisms like sandboxing processes, separating rendering engines from JavaScript execution, and implementing strict security boundaries. This existing security-focused architecture makes browsers exceptionally well-suited for unikernel deployments. While unikernels typically lack internal security boundaries, browsers compensate with their own robust isolation mechanisms. The browser's multi-process architecture and sandboxing capabilities effectively compensate for this limitation of unikernels. By combining these technologies, we get the best of both worlds. The minimal attack surface and resource efficiency of unikernels, complemented by the browser's battle-tested security architecture designed specifically for handling untrusted content, creates an ideal environment for secure, efficient browser infrastructure. ## Capabilities Our browsers-on-unikernels approach unlocks several capabilities unique to our platform: ### Intelligent standby mode When your app isn't actively processing network traffic, our browsers automatically go into standby mode. This dramatically reduces resource consumption during idle periods. Unlike traditional "sleep" states, our standby mode preserves the entire browser state while consuming negligible resources, allowing you to maintain long-running sessions without the associated costs. ### Persistent state through snapshots One of the most powerful features of our approach is the ability to create complete snapshots of the browser's state when entering standby mode. These snapshots capture everything: * Authentication cookies and session data * Loaded page content and DOM state * Browser window configuration and zoom level * Local storage * Active downloads and processes When the browser needs to be reactivated, the Kernel platform restores from these snapshots in milliseconds, allowing you to pick up exactly where you left off. ### Sub-millisecond cold starts Traditional browser infrastructure takes several seconds to initialize a new browser instance. Our unikernel-based approach achieves cold start times of less than 20 milliseconds. This ultra-fast startup capability makes our platform ideal for event-driven scenarios where rapid response to triggers is essential. Your app can spring to life almost instantly when needed, then return to standby when idle. ## Use cases Use our platform for anything that requires fast execution, low latency, or long-running browsers: * Long-running browser automation tasks with intermittent activity * Applications requiring browser state across invocations * Event-driven scenarios demanding ultra-low latency response * Resource-intensive browser automation at scale * Security-critical browser automation where isolation is paramount ## Getting started Sign up for an API key [here](https://dashboard.onkernel.com/sign-up). Our core images are open source on [Github](https://github.com/onkernel/kernel-images). # Zero Data Retention Source: https://kernel.sh/docs/info/zero-data-retention Zero data retention (ZDR) is an **enterprise-only** feature. When enabled for your org, Kernel operates select surfaces in a zero-retention mode so session data isn't retained after the browser terminates. This suppresses persistence of: * Session recordings * Live view streams * Telemetry ## Get access ZDR is configured per-org and isn't enabled by default. Enterprise customers can [contact Kernel support](/docs/info/support) to scope it, including the surfaces you need suppressed and your expected volume. # 1Password Source: https://kernel.sh/docs/integrations/1password Use credentials from your 1Password vaults for Managed Auth Connect 1Password to use credentials from your existing vaults with [Managed Auth](/docs/auth/overview). You don't need to recreate credentials in Kernel because 1Password items are discovered by domain matching. ## How It Works 1. **Connect a service account** — Add your 1Password service account token in the dashboard 2. **Domain matching** — When Managed Auth needs credentials, it searches your connected vaults for items matching the target domain 3. **Automatic fill** — Credentials (including TOTP secrets) are used to complete authentication Credentials are retrieved securely at authentication time. Values are never stored in Kernel—they remain in 1Password. ## Setup [Create a service account](https://start.1password.com/developer-tools/infrastructure-secrets/serviceaccount/?source=dev-portal) in 1Password with access to the vaults containing your login credentials. Copy the service account token (starts with `ops_`). Go to **Integrations** in the Kernel dashboard and click **Connect 1Password**. Give your provider a name (e.g., `my-1p`) and paste your service account token. Kernel will validate the connection and show which vaults are accessible. You can connect multiple 1Password accounts with different names. Reference your 1Password provider in the `credential` object. You can either specify an explicit item path or use auto-lookup by domain. ```typescript TypeScript theme={null} // Option 1: Auto-lookup by domain const auth = await kernel.auth.connections.create({ domain: 'github.com', profile_name: 'my-github-profile', credential: { provider: 'my-1p', auto: true }, }); // Option 2: Explicit item path (VaultName/ItemName) const auth = await kernel.auth.connections.create({ domain: 'github.com', profile_name: 'my-github-profile', credential: { provider: 'my-1p', path: 'Engineering/github-login' }, }); const login = await kernel.auth.connections.login(auth.id); ``` ```python Python theme={null} # Option 1: Auto-lookup by domain auth = await kernel.auth.connections.create( domain="github.com", profile_name="my-github-profile", credential={"provider": "my-1p", "auto": True}, ) # Option 2: Explicit item path (VaultName/ItemName) auth = await kernel.auth.connections.create( domain="github.com", profile_name="my-github-profile", credential={"provider": "my-1p", "path": "Engineering/github-login"}, ) login = await kernel.auth.connections.login(auth.id) ``` ```go Go theme={null} // Option 1: Auto-lookup by domain auth, err := client.Auth.Connections.New(ctx, kernel.AuthConnectionNewParams{ ManagedAuthCreateRequest: kernel.ManagedAuthCreateRequestParam{ Domain: "github.com", ProfileName: "my-github-profile", Credential: kernel.ManagedAuthCreateRequestCredentialParam{ Provider: kernel.String("my-1p"), Auto: kernel.Bool(true), }, }, }) if err != nil { panic(err) } // Option 2: Explicit item path (VaultName/ItemName) auth, err = client.Auth.Connections.New(ctx, kernel.AuthConnectionNewParams{ ManagedAuthCreateRequest: kernel.ManagedAuthCreateRequestParam{ Domain: "github.com", ProfileName: "my-github-profile", Credential: kernel.ManagedAuthCreateRequestCredentialParam{ Provider: kernel.String("my-1p"), Path: kernel.String("Engineering/github-login"), }, }, }) if err != nil { panic(err) } login, err := client.Auth.Connections.Login(ctx, auth.ID, kernel.AuthConnectionLoginParams{}) if err != nil { panic(err) } _ = login ``` ## Path Format When using explicit paths, specify `VaultName/ItemName`: ```typescript theme={null} credential: { provider: 'my-1p', path: 'Engineering/github-login' } ``` Vault and item names containing forward slashes (`/`) are not supported. Rename items in 1Password if needed. ## Domain Matching 1Password items are matched by their website/URL field: | 1Password Item URL | Matches Domain | | -------------------------- | ------------------------------------ | | `github.com` | `github.com` | | `https://github.com/login` | `github.com` | | `*.example.com` | `app.example.com`, `api.example.com` | If multiple items match a domain, the first match is used. Organize your vaults to ensure the correct credentials are selected. ## TOTP Support If your 1Password item has a one-time password (TOTP) field configured, Kernel can generate fresh codes for automatic login and reauthentication. No additional setup is needed. ## Supported Login Types Managed Auth fills **direct logins** from 1Password items: username and password credentials plus any TOTP field for 2FA. These direct flows support automatic reauthentication, including TOTP when its secret is stored in 1Password. This includes signing directly into an identity provider itself—for example, logging into a Google account with its stored username, password, and TOTP. 1Password's linked-item "sign in with" references are not supported. When an item delegates authentication to a separate item—for example a site item set to **sign in with** another login—that link is not exposed through the 1Password API, so Managed Auth can't follow it to the underlying credential. Store a direct login (username/password, plus a TOTP field if needed) for the target site instead. ## Credential Options The `credential` object supports multiple sources: | Type | Example | Description | | ------------------ | ------------------------------------------- | --------------------------------- | | Kernel credential | `{ name: 'my-creds' }` | Use a credential stored in Kernel | | 1Password explicit | `{ provider: 'my-1p', path: 'Vault/Item' }` | Use a specific 1Password item | | 1Password auto | `{ provider: 'my-1p', auto: true }` | Search 1Password by domain | If no `credential` is specified, the flow will wait for manual input. ## Security | Feature | Description | | ------------------------- | ------------------------------------------------------ | | **Token encrypted** | Service account token encrypted with per-org keys | | **No credential storage** | Credentials stay in 1Password, retrieved at auth time | | **Vault access control** | Limit access via 1Password service account permissions | | **Audit trail** | 1Password logs all credential access | # Browser Use Source: https://kernel.sh/docs/integrations/browser-use [Browser Use](https://github.com/browser-use/browser-use) is the AI browser agent that empowers anyone to automate repetitive online tasks, no code required. By integrating with Kernel, you can run Browser Use Agents and automations with cloud-hosted browsers. A complete, runnable example that drives the Browser Use bu-1.0 model on Kernel browsers lives in the [Browser Use + Kernel cookbook](https://github.com/kernel/cookbooks/tree/main/integrations/browser-use-model). ## Adding Kernel to existing Browser Use implementations If you already have a Browser Use implementation, you can easily switch to using Kernel's cloud browsers by updating your Browser definition. ### 1. Install the Kernel SDK ```bash theme={null} uv add kernel ``` ### 2. Initialize Kernel and create a browser Import the libraries and create a cloud browser session: ```python theme={null} from kernel import Kernel from browser_use import Browser, Agent # Initialize Kernel client kernel = Kernel() # Create a Kernel browser session kernel_browser = kernel.browsers.create() ``` ### 3. Update your Browser definition Replace your existing Browser initialization to use Kernel's CDP URL and display settings: ```python theme={null} # Update your Browser definition to use Kernel's CDP URL browser = Browser( cdp_url=kernel_browser.cdp_ws_url, headless=False, window_size={'width': 1920, 'height': 1080}, viewport={'width': 1920, 'height': 1080}, device_scale_factor=1.0 ) ``` Browser Use supports a wide range of browser configuration parameters. See the full list in the Browser Use docs. When running on Kernel, remember that browsers must use one of Kernel's supported viewport sizes and refresh rates. See Viewports for the supported configurations. ### 4. Create and run your agent Use your existing Agent setup with the Kernel-powered browser: ```python theme={null} # Use with your existing Agent setup agent = Agent( task="Your automation task", llm=your_llm_instance, browser_session=browser ) # Run your automation result = agent.run() # Clean up kernel.browsers.delete_by_id(kernel_browser.session_id) ``` ## Quick setup with our Browser Use example app Alternatively, you can use our Kernel app template that includes a pre-configured Browser Use integration: ```bash theme={null} kernel create --name my-browser-use-app --language python --template browser-use ``` Then follow the [deploy](/docs/apps/deploy) and [invoke](/docs/apps/invoke) guides to deploy and run your Browser Use automation on Kernel's infrastructure. ## Benefits of using Kernel with Browser Use * **No local browser management**: Run automations without installing or maintaining browsers locally * **Scalability**: Launch multiple browser sessions in parallel * **Stealth mode**: Built-in anti-detection features for web scraping * **Session state**: Maintain browser state across runs via [Profiles](/docs/auth/profiles) * **Live view**: Debug your automations with real-time browser viewing ## Next steps * Check out [live view](/docs/browsers/live-view) for debugging your automations * Learn about [stealth mode](/docs/browsers/bot-detection/stealth) for avoiding detection * Learn how to properly [terminate browser sessions](/docs/browsers/termination) * Learn how to [deploy](/docs/apps/deploy) your Browser Use app to Kernel # Claude Agent SDK Source: https://kernel.sh/docs/integrations/claude/claude-agent-sdk The [Claude Agent SDK](https://platform.claude.com/docs/en/agent-sdk/overview) provides a powerful way to build AI agents that can autonomously perform tasks. By integrating the Claude Agent SDK with Kernel, you can create agents that browse the web and interact with websites using cloud-hosted browser infrastructure. This integration combines Claude's agent capabilities with Kernel's Playwright Execution API to enable browser automation without managing local browser infrastructure. ## Quick setup with Claude Agent SDK Get started with Claude Agent SDK and Kernel using our pre-configured app template: ```bash theme={null} kernel create --template claude-agent-sdk ``` Choose `TypeScript` or `Python` as the programming language. Then follow the [deploy](/docs/apps/deploy) and [invoke](/docs/apps/invoke) guides to deploy and run your Claude Agent SDK automation on Kernel's infrastructure. ## Prerequisites ### Claude Code Installation The Claude Agent SDK requires Claude Code to be installed. Choose one of the following methods: ```bash theme={null} # Homebrew (macOS) brew install --cask claude-code # pnpm (cross-platform) pnpm add -g @anthropic-ai/claude-code # macOS/Linux/WSL curl -fsSL https://claude.ai/install.sh | bash ``` See the [official installation guide](https://platform.claude.com/docs/en/agent-sdk/overview#get-started) for Windows and other options. When deploying to Kernel, the app automatically installs Claude Code on the remote infrastructure. ### API Keys You'll need: * **ANTHROPIC\_API\_KEY**: Get from the [Anthropic Console](https://console.anthropic.com/) * **KERNEL\_API\_KEY**: Get from the [Kernel Dashboard](https://dashboard.onkernel.com/api-keys) ## Running locally ```bash TypeScript theme={null} # Install dependencies pnpm install # Set up environment variables cp .env.example .env # Edit .env with your API keys # Run with default task pnpm exec tsx index.ts # Run with custom task pnpm exec tsx index.ts "Go to duckduckgo.com and search for 'Kernel browser automation'" ``` ```bash Python theme={null} # Install dependencies uv sync # Set up environment variables cp .env.example .env # Edit .env with your API keys # Run with default task uv run main.py # Run with custom task uv run main.py "Go to duckduckgo.com and search for 'Kernel browser automation'" ``` ## Deploying to Kernel Deploy and invoke the app on Kernel's infrastructure: ```bash TypeScript theme={null} # Login to Kernel kernel login # Deploy the app with environment variables kernel deploy index.ts --env-file .env # Invoke the action (logs stream automatically) kernel invoke ts-claude-agent-sdk agent-task -p '{"task": "Go to https://news.ycombinator.com and get the top 3 stories"}' ``` ```bash Python theme={null} # Login to Kernel kernel login # Deploy the app with environment variables kernel deploy main.py --env-file .env # Invoke the action (logs stream automatically) kernel invoke py-claude-agent-sdk agent-task -p '{"task": "Go to https://news.ycombinator.com and get the top 3 stories"}' ``` ## How it works 1. **Browser Creation**: A Kernel browser session is created with stealth mode enabled 2. **MCP Server**: An in-process MCP server is created with an `execute_playwright` tool 3. **Agent Execution**: The Claude Agent SDK runs with access to the Playwright tool 4. **Task Completion**: Claude autonomously uses the tool to complete the given task 5. **Cleanup**: The browser session is deleted when done ## Example tasks ```bash theme={null} # Get top Hacker News stories "Go to https://news.ycombinator.com and tell me the top 3 stories" # Search for something "Go to duckduckgo.com and search for 'Kernel browser automation'" # Extract data from a page "Go to https://github.com/trending and list the top 5 trending repositories" ``` ## Benefits of using Kernel with Claude Agent SDK * **No local browser management**: Run Claude Agent SDK automations without installing or maintaining browsers locally * **Scalability**: Launch multiple browser sessions in parallel for concurrent AI agents * **Stealth mode**: Built-in anti-detection features for reliable web interactions * **Session state**: Maintain browser state across runs via [Profiles](/docs/auth/profiles) * **Live view**: Debug your Claude agents with real-time browser viewing * **Cloud infrastructure**: Run computationally intensive AI agents without local resource constraints ## Next steps * Check out [live view](/docs/browsers/live-view) for debugging your Claude Agent SDK automations * Learn about [stealth mode](/docs/browsers/bot-detection/stealth) for avoiding detection * Learn about [Playwright Execution](/docs/browsers/playwright-execution) for running Playwright code in the browser VM * Learn how to properly [terminate browser sessions](/docs/browsers/termination) * Learn how to [deploy](/docs/apps/deploy) your Claude Agent SDK app to Kernel # Claude Code and Desktop Source: https://kernel.sh/docs/integrations/claude/claude-code-and-desktop Give Claude Code and Claude Desktop a Kernel cloud browser Connect the Claude apps you already use to Kernel. ## Claude Desktop Claude Desktop reaches Kernel through Kernel's [official connector](https://claude.ai/directory/kernel), added as a custom connector under **Settings → Connectors**. The [Claude client guide](/docs/reference/mcp-server/clients/claude) has the steps, and covers Claude.ai as well. ## Marketplace plugin (Claude Code) Kernel publishes an [official skills marketplace](https://github.com/kernel/skills) for Claude Code. Installing a plugin from it loads Kernel's best-practice skills into your coding agent. Run these in the Claude Code REPL, one line at a time: ```bash theme={null} /plugin marketplace add kernel/skills /plugin install kernel-cli /plugin install kernel-sdks ``` Each plugin bundles a set of skills: * **`kernel-cli`** — managing browsers, apps, profiles, and proxies from the CLI, plus managed auth, session debugging, and bot-detection profiling. * **`kernel-sdks`** — building browser automation with the TypeScript and Python SDKs. See the [skills repo](https://github.com/kernel/skills#available-skills) for the current list. ## Remote MCP server (Claude Code) Add Kernel's [remote MCP server](/docs/reference/mcp-server) to manage cloud browsers from Claude Code. **Using the Kernel CLI (recommended):** ```bash theme={null} kernel mcp install --target claude-code # Then in the REPL run once to authenticate: /mcp ``` **Manual setup:** ```bash theme={null} claude mcp add --transport http kernel https://mcp.onkernel.com/mcp # Then in the REPL run once to authenticate: /mcp ``` # Claude Managed Agents Source: https://kernel.sh/docs/integrations/claude/claude-managed-agents [Claude Managed Agents](https://platform.claude.com/docs/en/managed-agents/overview) is Anthropic's hosted agent harness: you define an agent once, then start as many cloud sessions as you need, each running in an isolated Anthropic-managed sandbox. By pairing Claude Managed Agents with Kernel, your agents can browse the Web the way a person would. Kernel handles the work to spin up real browsers for the agent to engage with (including rendered pages, computer-use clicks, screenshots). The browsers are provisioned and controlled through the [Kernel CLI](/docs/reference/cli) from inside Anthropic's Managed Agents sandbox. Agents can fan out across many browsers in parallel, with no browser infrastructure to run yourself. This integration combines Claude's hosted agent runtime with Kernel's cloud browsers. Claude Managed Agents owns the **runtime** — it runs the agent loop in an Anthropic-managed sandbox with bash, file tools, and any custom tools you declare. Kernel owns the **browser** — a fresh, isolated, stealth Chromium session per run that the agent drives to load, read, and act on real pages. Each side handles what it's best at, and you operate neither. This is different from the [Claude Agent SDK](/docs/integrations/claude/claude-agent-sdk) integration. The **Agent SDK** is a library you run on your own machine or deploy as a Kernel app. **Managed Agents** is Anthropic's *hosted* harness — agents, sessions, environments, and vaults all live on Anthropic's side, and the agent reaches Kernel over the network. Use this page when you want Anthropic to host the agent loop. Claude Managed Agents is in beta — every request carries the `managed-agents-2026-04-01` beta header, which the Anthropic SDKs set automatically. The `environment_variable` vault credential used below is a narrower research preview; [request access](https://claude.com/form/claude-managed-agents) to enable it. Preview identifiers may change before general availability. ## Benefits of using Kernel with Claude Managed Agents * **No infrastructure to run**: Anthropic hosts the agent loop and Kernel hosts the browsers — no cold starts, container orchestration, or Chromium version pinning on your side. * **Clean separation of state**: Managed Agents holds the conversation and tool outputs; Kernel holds the page, cookies, and downloads — so you can inspect each side on its own. * **Parallel by default**: fan out one browser per subagent and run them concurrently, so a single coordinator can cover many pages at once. * **Stealth, non-headless browsing**: [stealth mode](/docs/browsers/bot-detection/stealth) drives real Chromium that renders and behaves like a human visitor, not a flagged bot. * **Managed Authentication**: Claude Managed Agents can seamlessly access browsers on behalf of users using Kernel's secure, permissioned abstraction of user identity. * **Persistent session state**: carry cookies and logins across runs with [Profiles](/docs/auth/profiles), so agents resume where they left off. * **Built-in observability**: watch agents drive their browsers live with [Live View](/docs/browsers/live-view), or review a run afterward with [session replays](/docs/browsers/replays). ## How it works A Managed Agents + Kernel setup has two kinds of resources: * **Durable resources**, created once and reused across runs: * an **environment** — the cloud sandbox the agent runs in, with the Kernel CLI preinstalled and outbound networking locked down, * a **vault** holding your `KERNEL_API_KEY` as a credential, * one or more **agents** — the system prompt, model, and tools that define behavior. * **Sessions**, which are ephemeral. Each session opens against an agent, attaches the vault, runs one task while you stream its events, and is deleted when done. Inside a session, the agent uses its built-in shell to run the Kernel CLI — e.g. `kernel browsers create --stealth`, `kernel browsers playwright execute`, `kernel browsers computer …` — to provision and drive cloud browsers. ## Prerequisites * **Node.js 18+** and the [Anthropic TypeScript SDK](https://github.com/anthropics/anthropic-sdk-typescript) (`@anthropic-ai/sdk`). * **ANTHROPIC\_API\_KEY**: get from the [Anthropic Console](https://console.anthropic.com/). Drives vaults, agents, and sessions. * **KERNEL\_API\_KEY**: get from the [Kernel Dashboard](https://dashboard.onkernel.com/api-keys). Stored in the vault and injected into the sandbox as a placeholder. * Early-access enrollment in Claude Managed Agents (including the `environment_variable` vault credential feature). ```bash theme={null} npm install @anthropic-ai/sdk export ANTHROPIC_API_KEY=... export KERNEL_API_KEY=... ``` The example code below is TypeScript. The same API is exposed through the Anthropic SDKs for other languages under their `beta` namespace as Managed Agents rolls out. ## Quickstart The complete, runnable version of this quickstart lives in the [Claude Managed Agents + Kernel cookbook](https://github.com/kernel/cookbooks/tree/main/integrations/claude-managed-agents), alongside more recipes for parallel computer-use agent swarms. The environment is the sandbox your agent runs in. Preinstall the Kernel CLI so workers can run `kernel …` immediately, and apply the environment-networking firewall from above. ```ts theme={null} import Anthropic from "@anthropic-ai/sdk"; const client = new Anthropic(); const KERNEL_HOSTS = ["api.onkernel.com", "*.onkernel.com"]; const env = await client.beta.environments.create({ name: "kernel-env", config: { type: "cloud", packages: { npm: ["@onkernel/cli"] }, networking: { type: "limited", allow_package_managers: true, allow_mcp_servers: false, allowed_hosts: KERNEL_HOSTS, }, }, }); ``` Create a vault, then add `KERNEL_API_KEY` as an `environment_variable` credential. The credential's `networking` block is the substitution allowlist — the real key is only injected into requests to these hosts. ```ts theme={null} const vault = await client.beta.vaults.create({ display_name: "Kernel vault", }); const credential = await client.beta.vaults.credentials.create(vault.id, { display_name: "Kernel API key", auth: { type: "environment_variable", secret_name: "KERNEL_API_KEY", secret_value: process.env.KERNEL_API_KEY!, networking: { type: "limited", allowed_hosts: KERNEL_HOSTS, }, }, // Enables environment_variable credentials during early access. betas: ["managed-agents-2026-06-11-research-preview"], }); ``` Give the worker a system prompt that drives Kernel browsers via the CLI, and disable `web_fetch` so it can only see a page through a real browser. Add a coordinator whose `multiagent` roster delegates to the worker for parallel runs. ```ts theme={null} const WORKER_SYSTEM_PROMPT = `You are a browser-automation engineer in a Linux sandbox. The Kernel CLI (@onkernel/cli, binary "kernel") is preinstalled and reads KERNEL_API_KEY automatically. KERNEL_API_KEY is an opaque placeholder — never echo, print, or log it. Create browsers with stealth and NEVER headless: kernel browsers create --stealth -o json Drive the page with Playwright + computer-use + screenshots, then clean up: kernel browsers playwright execute -o json 'await page.goto("https://example.com",{waitUntil:"load"}); return { title: await page.title() };' kernel browsers computer screenshot --to shot.png kernel browsers delete `; const worker = await client.beta.agents.create({ name: "browser-operator", model: "claude-haiku-4-5", system: WORKER_SYSTEM_PROMPT, tools: [ { type: "agent_toolset_20260401", configs: [{ name: "web_fetch", enabled: false }] }, ], }); const coordinator = await client.beta.agents.create({ name: "coordinator", model: "claude-haiku-4-5", system: "You lead a team of browser-operator subagents. Do light recon yourself, " + "then delegate exactly one page per operator and run them all in parallel. " + "Synthesize their findings into one report. Never print KERNEL_API_KEY.", tools: [ { type: "agent_toolset_20260401", configs: [{ name: "web_fetch", enabled: false }] }, ], multiagent: { type: "coordinator", agents: [{ type: "agent", id: worker.id }] }, }); ``` Create a session against the coordinator with the environment and vault attached, send the task, and stream events until the session goes idle. Every subagent thread inherits the vault, so each can use `KERNEL_API_KEY`. ```ts theme={null} const session = await client.beta.sessions.create({ agent: coordinator.id, environment_id: env.id, vault_ids: [vault.id], title: "UX audit", }); console.log(`Watch: https://platform.claude.com/workspaces/default/sessions/${session.id}`); const stream = await client.beta.sessions.events.stream(session.id); await client.beta.sessions.events.send(session.id, { events: [ { type: "user.message", content: [{ type: "text", text: "Run a parallel UX audit of https://example.com." }], }, ], }); for await (const event of stream) { if (event.type === "agent.message") { for (const block of event.content) { if (block.type === "text") process.stdout.write(block.text); } } else if (event.type === "agent.tool_use") { console.log(`\n[tool: ${event.name}]`); } else if (event.type === "session.status_idle") { if (event.stop_reason.type === "requires_action") continue; break; } } ``` Open the printed session URL to watch the agent — and its parallel subagents — drive their Kernel browsers live. Settle the session before deleting it (the idle event fires just before the status flips, so an immediate delete can 400), then delete it. Deleting the vault cascades to its credentials; environments delete directly. Agents have no delete — archive is the terminal state. ```ts theme={null} // Wait for the session to leave "running", then delete it. for (let i = 0; i < 15; i++) { const s = await client.beta.sessions.retrieve(session.id); if (s.status !== "running") break; await new Promise((r) => setTimeout(r, 1000)); } await client.beta.sessions.delete(session.id); await client.beta.vaults.delete(vault.id); // cascades to credentials await client.beta.environments.delete(env.id); await client.beta.agents.archive(coordinator.id); await client.beta.agents.archive(worker.id); ``` ## Next steps * Learn about [stealth mode](/docs/browsers/bot-detection/stealth) for reliable, non-headless browsing * Use [Playwright Execution](/docs/browsers/playwright-execution) to run structured Playwright from the CLI * Debug runs with [live view](/docs/browsers/live-view) * Persist browser state across sessions with [Managed Auth](/docs/auth) * Read the [Kernel CLI reference](/docs/reference/cli) for the full `kernel browsers` command surface # Overview Source: https://kernel.sh/docs/integrations/claude/overview Give Claude a Kernel cloud browser — across Claude Code, Claude Desktop, the Agent SDK, and Managed Agents ## Claude + Kernel Kernel gives Claude a real, cloud-hosted browser to work in, so your agents can load pages, read rendered content, and act on the web without you running any browser infrastructure. There are a few ways to connect Claude to Kernel depending on where your agent runs: ### Claude Code and Claude Desktop Connect the Claude apps you already use to Kernel. Add Kernel's [official connector](https://claude.ai/directory/kernel) to manage cloud browsers from Claude Desktop and Claude.ai, or install the [marketplace plugin](https://github.com/kernel/skills#claude-code) to load Kernel's CLI and SDK skills into Claude Code. [Set up Claude Code and Desktop →](/docs/integrations/claude/claude-code-and-desktop) ### Claude Agent SDK The [Claude Agent SDK](https://platform.claude.com/docs/en/agent-sdk/overview) is a library you run yourself — on your own machine or deployed as a Kernel app. Pair it with Kernel's Playwright Execution API to build agents that browse the web and interact with websites on cloud browsers. [Learn more about the Claude Agent SDK →](/docs/integrations/claude/claude-agent-sdk) ### Claude Managed Agents [Claude Managed Agents](https://platform.claude.com/docs/en/managed-agents/overview) is Anthropic's hosted agent harness: define an agent once, then start as many cloud sessions as you need. Claude runs the agent loop in an Anthropic-managed sandbox; Kernel provisions a fresh, isolated Chromium session per run that the agent drives. You run neither the agent loop nor the browser. [Learn more about Claude Managed Agents →](/docs/integrations/claude/claude-managed-agents) ## Next Steps Give the Claude apps a Kernel browser Run Agent SDK automations on cloud browsers Run Anthropic's hosted agent harness against cloud browsers # Anthropic Source: https://kernel.sh/docs/integrations/computer-use/anthropic [Computer Use](https://docs.claude.com/en/docs/agents-and-tools/tool-use/computer-use-tool) is Anthropic's groundbreaking capability that enables Claude to interact with computers the way humans do by looking at screens, moving cursors, clicking buttons, and typing text. This powerful feature allows AI agents to control web browsers, navigate interfaces, and perform complex tasks across applications. By integrating Computer Use with Kernel, you can run these AI-powered browser automations on cloud-hosted infrastructure, eliminating the need for local browser management and enabling scalable, reliable AI agents. A minimal, runnable implementation of Anthropic's computer use loop lives in the [Claude computer use loop cookbook](https://github.com/kernel/cookbooks/tree/main/integrations/claude-computer-use-loop). ## Quick setup with Computer Use Get started with Computer Use and Kernel using our pre-configured app template: ```bash theme={null} kernel create --name my-computer-use-app --template computer-use ``` Choose `TypeScript` or `Python` as the programming language. Then follow the [deploy](/docs/apps/deploy) and [invoke](/docs/apps/invoke) guides to deploy and run your Computer Use automation on Kernel's infrastructure. ## Build your own agent For full control over the loop, drive Claude from TypeScript with [`@onkernel/cua-agent`](/docs/integrations/computer-use/overview#build-your-own-agent): ```ts theme={null} import Kernel from "@onkernel/sdk"; import { CuaAgent } from "@onkernel/cua-agent"; const client = new Kernel({ apiKey: process.env.KERNEL_API_KEY! }); const browser = await client.browsers.create({ stealth: true }); const agent = new CuaAgent({ browser, client, initialState: { model: "anthropic:claude-opus-4-7", systemPrompt: "You are a careful browser automation agent.", }, }); await agent.prompt("Open news.ycombinator.com and summarize the top story."); ``` ## Benefits of using Kernel with Computer Use * **No local browser management**: Run Computer Use automations without installing or maintaining browsers locally * **Scalability**: Launch multiple browser sessions in parallel for concurrent AI agents * **Stealth mode**: Built-in anti-detection features for reliable web interactions * **Session state**: Maintain browser state across runs via [Profiles](/docs/auth/profiles) * **Live view**: Debug your Computer Use agents with real-time browser viewing * **Cloud infrastructure**: Run computationally intensive AI agents without local resource constraints ## Next steps * Check out [live view](/docs/browsers/live-view) for debugging your Computer Use automations * Learn about [stealth mode](/docs/browsers/bot-detection/stealth) for avoiding detection * Learn how to properly [terminate browser sessions](/docs/browsers/termination) * Learn how to [deploy](/docs/apps/deploy) your Computer Use app to Kernel # Gemini Source: https://kernel.sh/docs/integrations/computer-use/gemini [Gemini 2.5 Computer Use](https://blog.google/technology/google-deepmind/gemini-computer-use-model/) is Google's groundbreaking capability that enables AI models to interact with computers the way humans do by looking at screens, moving cursors, clicking buttons, and typing text. This powerful feature allows AI agents to control web browsers, navigate interfaces, and perform complex tasks across applications. By integrating Gemini 2.5 Computer Use with Kernel, you can run these AI-powered browser automations on cloud-hosted infrastructure, eliminating the need for local browser management and enabling scalable, reliable AI agents. ## Quick setup with Computer Use Get started with Gemini Computer Use and Kernel using our pre-configured app template: ```bash theme={null} kernel create --name my-computer-use-app --language typescript --template gemini-computer-use ``` Then follow the [deploy](/docs/apps/deploy) and [invoke](/docs/apps/invoke) guides to deploy and run your Computer Use automation on Kernel's infrastructure. ## Build your own agent For full control over the loop, drive Gemini from TypeScript with [`@onkernel/cua-agent`](/docs/integrations/computer-use/overview#build-your-own-agent): ```ts theme={null} import Kernel from "@onkernel/sdk"; import { CuaAgent } from "@onkernel/cua-agent"; const client = new Kernel({ apiKey: process.env.KERNEL_API_KEY! }); const browser = await client.browsers.create({ stealth: true }); const agent = new CuaAgent({ browser, client, initialState: { model: "google:gemini-3-flash-preview", systemPrompt: "You are a careful browser automation agent.", }, }); await agent.prompt("Open news.ycombinator.com and summarize the top story."); ``` ## Benefits of using Kernel with Computer Use * **No local browser management**: Run Computer Use automations without installing or maintaining browsers locally * **Scalability**: Launch multiple browser sessions in parallel for concurrent AI agents * **Stealth mode**: Built-in anti-detection features for reliable web interactions * **Session state**: Maintain browser state across runs via [Profiles](/docs/auth/profiles) * **Live view**: Debug your Computer Use agents with real-time browser viewing * **Cloud infrastructure**: Run computationally intensive AI agents without local resource constraints ## Next steps * Check out [live view](/docs/browsers/live-view) for debugging your Computer Use automations * Learn about [stealth mode](/docs/browsers/bot-detection/stealth) for avoiding detection * Learn how to properly [terminate browser sessions](/docs/browsers/termination) * Learn how to [deploy](/docs/apps/deploy) your Computer Use app to Kernel # OpenAGI Source: https://kernel.sh/docs/integrations/computer-use/openagi The [OpenAGI](https://lux.agiopen.org/) Lux Model is a computer-use model that enables AI agents to interact with computers the way humans do. Lux operates in a continuous action-observation loop: it receives a task, analyzes a screenshot of the current screen state, generates the next UI interaction (click, type, etc.), and repeats until the goal is achieved. By integrating OpenAGI with Kernel, you can run these AI-powered browser automations on cloud-hosted infrastructure, eliminating the need for local browser management and enabling scalable, reliable computer-using agents. ## Getting started To get started with OpenAGI Lux and Kernel: 1. Get your `OAGI_API_KEY` on the [OpenAGI Developer Platform](https://developer.agiopen.org/) 2. Get your `KERNEL_API_KEY` from the [Kernel Dashboard](https://dashboard.onkernel.com) For more information about Lux's capabilities, visit the [OpenAGI Lux Documentation](https://lux.agiopen.org/). ## Quick setup with Computer Use The fastest way to get started is using the Kernel CLI's built-in OpenAGI template: ```bash theme={null} kernel create --template openagi-computer-use --language python cd kernel deploy main.py --env-file .env ``` This creates a pre-configured OpenAGI app with both `AsyncDefaultAgent` and `TaskerAgent` implementations ready to deploy. ## Benefits of using Kernel with OpenAGI * **No local browser management**: Run OpenAGI automations without installing or maintaining browsers locally * **Scalability**: Launch multiple browser sessions in parallel for concurrent AI agents * **Stealth mode**: Built-in anti-detection features for reliable web interactions * **Session state**: Maintain browser state across runs via [Profiles](/docs/auth/profiles) * **Live view**: Debug your OpenAGI agents with real-time browser viewing * **Cloud infrastructure**: Run computationally intensive AI agents without local resource constraints ## Next steps * Check out [live view](/docs/browsers/live-view) for debugging your OpenAGI automations * Learn about [stealth mode](/docs/browsers/bot-detection/stealth) for avoiding detection * Learn how to properly [terminate browser sessions](/docs/browsers/termination) * Learn how to [deploy](/docs/apps/deploy) your OpenAGI app to Kernel # OpenAI Source: https://kernel.sh/docs/integrations/computer-use/openai [Computer Use](https://openai.com/index/computer-using-agent/) is OpenAI's feature that enables AI models to interact with computers the way humans do by looking at screens, moving cursors, clicking buttons, and typing text. This powerful feature allows AI agents to control web browsers, navigate interfaces, and perform complex tasks across applications. By integrating Computer Use with Kernel, you can run these AI-powered browser automations on cloud-hosted infrastructure, eliminating the need for local browser management and enabling scalable, reliable AI agents. ## Quick setup with our Computer Use example app Get started quickly with our Kernel app template that includes a pre-configured Computer Use integration: ```bash theme={null} kernel create --name my-computer-use-app --template cua ``` Choose `TypeScript` or `Python` as the programming language. Then follow the [deploy](/docs/apps/deploy) and [invoke](/docs/apps/invoke) guides to deploy and run your Computer Use automation on Kernel's infrastructure. ## Build your own agent For full control over the loop, drive OpenAI's CUA from TypeScript with [`@onkernel/cua-agent`](/docs/integrations/computer-use/overview#build-your-own-agent): ```ts theme={null} import Kernel from "@onkernel/sdk"; import { CuaAgent } from "@onkernel/cua-agent"; const client = new Kernel({ apiKey: process.env.KERNEL_API_KEY! }); const browser = await client.browsers.create({ stealth: true }); const agent = new CuaAgent({ browser, client, initialState: { model: "openai:gpt-5.5", systemPrompt: "You are a careful browser automation agent.", }, }); await agent.prompt("Open news.ycombinator.com and summarize the top story."); ``` ## Benefits of using Kernel with Computer Use * **No local browser management**: Run Computer Use automations without installing or maintaining browsers locally * **Scalability**: Launch multiple browser sessions in parallel for concurrent automations * **Stealth mode**: Built-in anti-detection features for web interactions * **Session state**: Maintain browser state across runs via [Profiles](/docs/auth/profiles) * **Live view**: Debug your automations with real-time browser viewing ## Next steps * Check out [live view](/docs/browsers/live-view) for debugging your automations * Learn about [stealth mode](/docs/browsers/bot-detection/stealth) for avoiding detection * Learn how to properly [terminate browser sessions](/docs/browsers/termination) * Learn how to [deploy](/docs/apps/deploy) your Computer Use app to Kernel # Overview Source: https://kernel.sh/docs/integrations/computer-use/overview Run computer use agents on Kernel cloud browsers Computer use models are vision-language models (VLMs) that operate a browser the way a person does: they look at a screenshot, decide what to do next, and emit a concrete action: move the mouse, click, type, scroll, or drag. Kernel runs these agents on cloud browsers, so you don't install or maintain anything locally, and gives the model the low-level [Computer Controls API](/docs/browsers/computer-controls) it needs to see the screen and act on it. ## How computer use works on Kernel Every computer use integration runs the same action-observation loop: 1. **Capture** a screenshot of the current browser state with the [Computer Controls API](/docs/browsers/computer-controls#take-screenshots). 2. **Predict** the next action by sending that screenshot to your model. 3. **Execute** the returned action (click, type, scroll, drag, or key press) through Computer Controls. 4. **Repeat** until the task is complete. Computer Controls emulates native keyboard and mouse input at the OS level (with human-like [Bézier curves](/docs/browsers/computer-controls#move-the-mouse) by default) instead of driving the page over the Chrome DevTools Protocol (CDP). This keeps the loop close to real user input and reduces the automation signals that [bot detection](/docs/browsers/bot-detection/overview) systems look for. The loop works with any VLM that predicts actions from pixels. The models below are the ones we maintain ready-to-deploy templates and guides for. ## Supported models Claude's computer use tool Google's Gemini 2.5 Computer Use model OpenAGI's Lux model OpenAI's computer-using agent (CUA) Tzafon's Northstar CUA Fast model Yutori's Navigator n1.5 pixels-to-actions model Using a model that isn't listed here? Any VLM works; wire its predicted actions straight to the [Computer Controls API](/docs/browsers/computer-controls) and run the same loop. ## Get started Each model page includes a one-command template so you can deploy a working agent in minutes. For example, to scaffold the Anthropic integration: ```bash theme={null} kernel create --name my-computer-use-app --template computer-use ``` Pick a model above to get its template, then follow the [deploy](/docs/apps/deploy) and [invoke](/docs/apps/invoke) guides to run your agent on Kernel. ## Build your own agent For full control over the loop, [`@onkernel/cua-agent`](https://github.com/kernel/cua/tree/main/packages/agent) is a TypeScript library that runs it against a Kernel browser for you. You point it at a model, give it a task, and it handles the screenshots, actions, and follow-up turns. ```bash theme={null} npm install @onkernel/cua-agent @onkernel/cua-ai @onkernel/sdk ``` ```ts theme={null} import Kernel from "@onkernel/sdk"; import { CuaAgent } from "@onkernel/cua-agent"; const client = new Kernel({ apiKey: process.env.KERNEL_API_KEY! }); const browser = await client.browsers.create({ stealth: true }); const agent = new CuaAgent({ browser, client, initialState: { model: "anthropic:claude-opus-4-7", // swap to target another provider systemPrompt: "You are a careful browser automation agent.", }, }); await agent.prompt("Open news.ycombinator.com and summarize the top story."); ``` Switch providers by changing the `model` ref: | Provider | Model ref | | --------- | ---------------------------------- | | Anthropic | `anthropic:claude-opus-4-7` | | OpenAI | `openai:gpt-5.5` | | Gemini | `google:gemini-3-flash-preview` | | Tzafon | `tzafon:tzafon.northstar-cua-fast` | | Yutori | `yutori:n1.5-latest` | Set the matching provider key (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GOOGLE_API_KEY`, `TZAFON_API_KEY`, or `YUTORI_API_KEY`) alongside `KERNEL_API_KEY`. ## Benefits of using Kernel for computer use * **No local browser management**: Run computer use automations without installing or maintaining browsers locally * **Scalability**: Launch multiple browser sessions in parallel for concurrent AI agents * **Stealth mode**: Built-in anti-detection features for reliable web interactions * **Session state**: Maintain browser state across runs via [Profiles](/docs/auth/profiles) * **Live view**: Debug your agents with real-time browser viewing * **Cloud infrastructure**: Run computationally intensive AI agents without local resource constraints ## Next steps * Read the [Computer Controls API](/docs/browsers/computer-controls) reference for the full set of mouse, keyboard, and screenshot actions * Check out [live view](/docs/browsers/live-view) for debugging your automations * Learn about [stealth mode](/docs/browsers/bot-detection/stealth) for avoiding detection * Learn how to properly [terminate browser sessions](/docs/browsers/termination) * Learn how to [deploy](/docs/apps/deploy) your computer use app to Kernel # Tzafon Source: https://kernel.sh/docs/integrations/computer-use/tzafon Northstar CUA Fast is [Tzafon's](https://www.tzafon.ai/) vision language model trained with reinforcement learning for computer use tasks. It implements a CUA (computer use agent) loop that predicts browser actions from screenshots, enabling AI agents to interact with web interfaces. The model is accessed via Tzafon's [Lightcone](https://docs.lightcone.ai/) API platform. By integrating Tzafon with Kernel, you can run these AI-powered browser automations on cloud-hosted infrastructure using Kernel's Computer Controls API, eliminating the need for local browser management and enabling scalable, reliable AI agents. ## Quick setup with our Tzafon example app Get started quickly with our Kernel app template that includes a pre-configured Tzafon Northstar CUA Fast integration: ```bash theme={null} kernel create --name my-tzafon-app --template tzafon ``` Choose `TypeScript` or `Python` as the programming language. Then follow the [deploy](/docs/apps/deploy) and [invoke](/docs/apps/invoke) guides to deploy and run your Tzafon automation on Kernel's infrastructure. ## Build your own agent For full control over the loop, drive Northstar CUA Fast from TypeScript with [`@onkernel/cua-agent`](/docs/integrations/computer-use/overview#build-your-own-agent): ```ts theme={null} import Kernel from "@onkernel/sdk"; import { CuaAgent } from "@onkernel/cua-agent"; const client = new Kernel({ apiKey: process.env.KERNEL_API_KEY! }); const browser = await client.browsers.create({ stealth: true }); const agent = new CuaAgent({ browser, client, initialState: { model: "tzafon:tzafon.northstar-cua-fast", systemPrompt: "You are a careful browser automation agent.", }, }); await agent.prompt("Open news.ycombinator.com and summarize the top story."); ``` ## Benefits of using Kernel with Tzafon Northstar CUA Fast * **No local browser management**: Run Northstar CUA Fast automations without installing or maintaining browsers locally * **Scalability**: Launch multiple browser sessions in parallel for concurrent AI agents * **Stealth mode**: Built-in anti-detection features for reliable web interactions * **Session state**: Maintain browser state across runs via [Profiles](/docs/auth/profiles) * **Live view**: Debug your Tzafon agents with real-time browser viewing * **Cloud infrastructure**: Run computationally intensive AI agents without local resource constraints ## Next steps * Check out [live view](/docs/browsers/live-view) for debugging your automations * Learn about [stealth mode](/docs/browsers/bot-detection/stealth) for avoiding detection * Learn how to properly [terminate browser sessions](/docs/browsers/termination) * Learn how to [deploy](/docs/apps/deploy) your Tzafon app to Kernel * Read the [Lightcone API documentation](https://docs.lightcone.ai/) for model details # Yutori Source: https://kernel.sh/docs/integrations/computer-use/yutori [Navigator n1.5](https://yutori.com/blog/introducing-n1-5) is Yutori's pixels-to-actions LLM that predicts browser actions from screenshots. This computer use model enables AI agents to interact with web interfaces by analyzing visual content and generating appropriate mouse and keyboard actions. By integrating Yutori n1.5 with Kernel, you can run these AI-powered browser automations on cloud-hosted infrastructure, eliminating the need for local browser management and enabling scalable, reliable AI agents. ## Quick setup with our Yutori example app Get started quickly with our Kernel app template that includes a pre-configured Yutori n1.5 integration: ```bash theme={null} kernel create --name my-yutori-app --template yutori ``` Choose `TypeScript` or `Python` as the programming language. Then follow the [deploy](/docs/apps/deploy) and [invoke](/docs/apps/invoke) guides to deploy and run your Yutori automation on Kernel's infrastructure. ## Build your own agent For full control over the loop, drive Navigator n1.5 from TypeScript with [`@onkernel/cua-agent`](/docs/integrations/computer-use/overview#build-your-own-agent): ```ts theme={null} import Kernel from "@onkernel/sdk"; import { CuaAgent } from "@onkernel/cua-agent"; const client = new Kernel({ apiKey: process.env.KERNEL_API_KEY! }); const browser = await client.browsers.create({ stealth: true }); const agent = new CuaAgent({ browser, client, initialState: { model: "yutori:n1.5-latest", systemPrompt: "You are a careful browser automation agent.", }, }); await agent.prompt("Open news.ycombinator.com and summarize the top story."); ``` ## Benefits of using Kernel with Yutori n1.5 * **No local browser management**: Run n1.5 automations without installing or maintaining browsers locally * **Scalability**: Launch multiple browser sessions in parallel for concurrent AI agents * **Stealth mode**: Built-in anti-detection features for reliable web interactions * **Session state**: Maintain browser state across runs via [Profiles](/docs/auth/profiles) * **Live view**: Debug your Yutori agents with real-time browser viewing * **Cloud infrastructure**: Run computationally intensive AI agents without local resource constraints ## Next steps * Check out [live view](/docs/browsers/live-view) for debugging your automations * Learn about [stealth mode](/docs/browsers/bot-detection/stealth) for avoiding detection * Learn how to properly [terminate browser sessions](/docs/browsers/termination) * Learn how to [deploy](/docs/apps/deploy) your Yutori app to Kernel * Read the [Yutori n1.5 API documentation](https://docs.yutori.com/reference/n1-5) for model details # Hermes Agent Source: https://kernel.sh/docs/integrations/hermes-agent Run Hermes Agent browser tools on Kernel cloud browsers [Hermes Agent](https://github.com/NousResearch/hermes-agent) is an AI agent with browser automation, terminal access, skills, and messaging integrations. The [Kernel browser plugin](https://github.com/kernel/hermes-browser-plugin) routes Hermes's `browser_*` tools through Kernel cloud browsers and handles browser creation and cleanup for you. ## Install the plugin Install and enable the plugin from the Kernel repository: ```bash theme={null} hermes plugins install kernel/hermes-browser-plugin --enable ``` You need: * A current Hermes Agent installation with browser-provider plugin support * A [Kernel API key](https://dashboard.onkernel.com/api-keys) * Node.js 20 or newer for Hermes's `agent-browser` dependency Some Hermes versions prompt for `KERNEL_API_KEY` during plugin installation and some don't. Selecting Kernel as the browser provider doesn't set the API key. ## Configure Hermes Desktop 1. Open **Capabilities → Tools → Browser Automation**. 2. Select **Kernel**. 3. If `agent_browser` isn't installed, click **Run setup** and wait for the one-time installation to finish. 4. Open the three-dot menu next to `KERNEL_API_KEY` and enter your [Kernel API key](https://dashboard.onkernel.com/api-keys). 5. Optionally set `KERNEL_PROXY_NAME` and `KERNEL_PROFILE_NAME` from their three-dot menus. 6. In a terminal, run the following command so Hermes's cleanup timer matches the plugin's 10-minute Kernel browser timeout: ```bash theme={null} hermes config set browser.inactivity_timeout 600 ``` 7. Restart any running Hermes chat or gateway so it reads the updated settings. ## Configure the Hermes CLI Select Kernel and match Hermes's cleanup timeout to the plugin's browser timeout: ```bash theme={null} hermes config set browser.cloud_provider kernel hermes config set browser.inactivity_timeout 600 ``` If plugin installation didn't prompt for your API key, add `KERNEL_API_KEY` to the active Hermes profile's `.env` file. This command prints the file's location: ```bash theme={null} hermes config env-path ``` Install `agent-browser` once if Hermes hasn't installed it yet: ```bash theme={null} hermes tools post-setup agent_browser ``` Verify your setup: ```bash theme={null} hermes plugins list hermes config show hermes doctor ``` ## Configure a default profile or proxy The Kernel provider supports these settings in the active Hermes profile: | Variable | Required | Purpose | | --------------------- | -------- | ------------------------------------------------------ | | `KERNEL_API_KEY` | Yes | Authenticates requests to Kernel | | `KERNEL_PROFILE_NAME` | No | Loads an existing Kernel profile by name | | `KERNEL_PROXY_NAME` | No | Resolves and attaches an existing Kernel proxy by name | When you set `KERNEL_PROFILE_NAME`, the plugin saves browser changes back to that profile when the browser ends. This preserves cookies, logins, and other session state for future runs. ## Understand browser cleanup The plugin creates every Kernel browser with a 10-minute inactivity timeout. Hermes also maintains its own browser cleanup timer, which defaults to two minutes and refreshes whenever Hermes runs a `browser_*` tool. Matching it to 10 minutes prevents Hermes from deleting the browser before Kernel's timeout. When Hermes cleans up a session, it closes `agent-browser`, deletes the Kernel browser, and removes the session from its local registry. Hermes also runs this cleanup when its process exits normally. Kernel's 10-minute timeout remains the remote fallback if Hermes crashes or loses its connection. [Kernel standby](/docs/browsers/standby) incurs zero browser usage cost only when no CDP, WebDriver, live-view, or computer-controls client is connected. Hermes maintains an `agent-browser` CDP session while it tracks the browser, so a longer Hermes cleanup timeout improves continuity but can also keep the browser active longer. ## Verify browser routing Start Hermes with browser tools enabled: ```bash theme={null} hermes chat --verbose --toolsets browser ``` Then ask Hermes: > Use `browser_navigate`, not web search, to open [https://example.com](https://example.com). Call `browser_snapshot`, report the exact title and `stealth_features`, and tell me whether any cloud fallback warning occurred. A successful run has these results: 1. The page title is `Example Domain`. 2. `stealth_features` includes `stealth`. 3. The tool result has no `fallback_warning`. 4. A matching browser appears in the [Kernel Dashboard](https://dashboard.onkernel.com/browsers). 5. Exiting Hermes or waiting for cleanup removes the browser. Hermes can fall back to local Chromium if a cloud provider fails. A successful navigation alone doesn't confirm that Hermes used Kernel; verify both the tool result and the Kernel Dashboard. ## Update the plugin Pull the latest version and restart Hermes Desktop, your active chat, or the gateway: ```bash theme={null} hermes plugins update browser-kernel ``` ## Next steps * Use [Live View](/docs/browsers/live-view) to inspect an active Hermes browser * Create reusable browser state with [Profiles](/docs/auth/profiles) * Configure a named [Proxy](/docs/proxies/overview) * Learn how Kernel [terminates browser sessions](/docs/browsers/termination) # Laminar Source: https://kernel.sh/docs/integrations/laminar [Laminar](https://www.laminar.sh/) is an open-source observability and evaluation platform for autonomous AI agents. You can create a cloud account or self-host Laminar for your infrastructure. By integrating Laminar with Kernel, you can trace and monitor your browser automations with full visibility into LLM calls, browser actions, session recordings, and performance metrics. ## Why use Laminar with Kernel? * **No local browser management**: Run automations in the cloud while maintaining full observability * **Scalability**: Launch multiple browser sessions with independent traces * **Debugging**: Use Kernel's [live view](/docs/browsers/live-view) during development and Laminar's session recordings for post-execution analysis * **Cost optimization**: Track LLM costs across all your browser automations * **Performance tuning**: Identify slow operations and optimize your agent workflows ## Prerequisites Before integrating Laminar with Kernel, you'll need: 1. A [Kernel account](https://dashboard.onkernel.com/sign-up) with a Kernel API Key 2. A [Laminar account](https://www.laminar.sh/) and project 3. Your Laminar project API key from the Project Settings page ## Installation ```bash npm theme={null} npm install @lmnr-ai/lmnr @onkernel/sdk ``` ```bash python theme={null} uv pip install --upgrade 'lmnr[all]' kernel ``` ## Getting your Laminar API key 1. Log in to your [Laminar dashboard](https://www.laminar.sh/) 2. Navigate to **Project Settings** 3. Generate a new API key in your project 4. Copy your **Project API Key** 5. Set it as an environment variable: ```bash theme={null} export LMNR_PROJECT_API_KEY=your_api_key_here ``` You will also need to generate a `KERNEL_API_KEY` from your [Kernel dashboard](https://dashboard.onkernel.com/api-keys) to authenticate with Kernel's browser infrastructure. ## Browser Agent Framework Examples Select your browser automation framework to enable Laminar tracing with Kernel: * [Playwright](#playwright) * [Browser Use](#browser-use) * [Stagehand](#stagehand) Always call `Laminar.flush()` or ensure your traced functions complete to submit traces to Laminar. ### Playwright Playwright is a popular low-level browser automation framework. Here's how to use it with Laminar and Kernel: The Playwright examples include `waitForTimeout()` calls to help ensure Laminar traces populate properly for these short, fast code snippets. ```javascript Typescript/Javascript theme={null} import { Laminar, observe } from '@lmnr-ai/lmnr'; import Kernel from '@onkernel/sdk'; import { chromium } from 'playwright'; // Initialize Laminar with Playwright instrumentation Laminar.initialize({ projectApiKey: process.env.LMNR_PROJECT_API_KEY, instrumentModules: { playwright: { chromium }, kernel: Kernel, } }); // Initialize Kernel and create a cloud browser const kernel = new Kernel(); const main = async () => observe({ name: 'main' }, async () => { const kernelBrowser = await kernel.browsers.create({ stealth: true }); console.log("Live view url:", kernelBrowser.browser_live_view_url); // Connect Playwright to Kernel's browser via CDP 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()); // Wait for 3 second await page.waitForTimeout(3000); // Your automation code await page.goto('https://www.onkernel.com/docs'); // Wait for 2 second await page.waitForTimeout(2000); // Navigate to careers page await page.goto('https://www.onkernel.com/docs/careers/intro'); // Extract all job URLs from the ul next to #open-roles const jobLinks = await page.locator('#open-roles + ul a').evaluateAll((links) => { const baseUrl = 'https://www.onkernel.com'; return links .map(link => { const href = link.getAttribute('href'); if (!href) return null; // Convert relative URLs to absolute URLs return href.startsWith('http') ? href : baseUrl + href; }) .filter(href => href !== null); }); console.log('Job URLs found:', jobLinks); console.log(`Total jobs: ${jobLinks.length}`); // Wait for 3 seconds await page.waitForTimeout(3000); // Clean up the browser await browser.close(); // Delete the browser for those who left open the live view url await kernel.browsers.deleteByID(kernelBrowser.session_id); }); await main(); await Laminar.flush(); ``` ```python Python theme={null} import os from lmnr import Laminar, observe from playwright.sync_api import sync_playwright from kernel import Kernel # Initialize Laminar Laminar.initialize(project_api_key=os.environ["LMNR_PROJECT_API_KEY"]) # Use @observe decorator to create a trace @observe() def run_automation(): # Initialize Kernel client = Kernel() kernel_browser = client.browsers.create(stealth=True) print(f"Live view url: {kernel_browser.browser_live_view_url}") # Connect Playwright to Kernel's browser with sync_playwright() as p: browser = p.chromium.connect_over_cdp(kernel_browser.cdp_ws_url) context = browser.contexts[0] if browser.contexts else browser.new_context() page = context.pages[0] if context.pages else context.new_page() # Wait for 3 seconds page.wait_for_timeout(3000) # Your automation code page.goto('https://www.onkernel.com/docs') # Wait for 3 seconds page.wait_for_timeout(3000) # Navigate to careers page page.goto('https://www.onkernel.com/docs/careers/intro') page.wait_for_timeout(3000) # Wait 2 seconds # Extract all job URLs from the ul next to #open-roles job_links = page.locator('#open-roles + ul a').evaluate_all(""" (links) => { const baseUrl = 'https://www.onkernel.com'; return links .map(link => { const href = link.getAttribute('href'); if (!href) return null; // Convert relative URLs to absolute URLs return href.startsWith('http') ? href : baseUrl + href; }) .filter(href => href !== null); } """) print(f'Job URLs found: {job_links}') print(f'Total jobs: {len(job_links)}') # Wait for 3 seconds page.wait_for_timeout(3000) # Clean up the browser browser.close() # Flush traces to Laminar Laminar.flush() # Delete the browser for those who left open the Kernel live view url client.browsers.delete_by_id(kernel_browser.session_id) # Run the automation run_automation() ``` ### Browser Use [Browser Use](https://github.com/browser-use/browser-use) is an AI browser agent framework. Here's how to integrate it with Laminar and Kernel: ```python python theme={null} import os import asyncio from lmnr import Laminar, observe from browser_use import Agent, Browser, ChatOpenAI from kernel import Kernel # Initialize Laminar Laminar.initialize(project_api_key=os.environ["LMNR_PROJECT_API_KEY"]) @observe() async def main(): # Initialize Kernel and create a browser client = Kernel() kernel_browser = client.browsers.create(stealth=True, viewport={'width': 1920, 'height': 1080}) print(f"Live view url: {kernel_browser.browser_live_view_url}") # Configure Browser Use with Kernel's CDP URL browser = Browser( cdp_url=kernel_browser.cdp_ws_url, headless=False, window_size={'width': 1920, 'height': 1080}, viewport={'width': 1920, 'height': 1080}, device_scale_factor=1.0 ) # Initialize the model llm = ChatOpenAI( model="gpt-4.1", ) # Create and run the agent with job extraction task agent = Agent( task="""1. Go to https://www.onkernel.com/docs 2. Navigate to the main Jobs page 3. Extract all the job posting URLs. List each URL you find.""", llm=llm, browser_session=browser ) result = await agent.run() print(f"Job URLs found:\n{result.final_result()}") # Flush traces to Laminar Laminar.flush() # Delete the browser for those who left open the live view url client.browsers.delete_by_id(kernel_browser.session_id) asyncio.run(main()) ``` ### Stagehand [Stagehand](https://github.com/browserbase/stagehand) is an AI browser automation framework. Here's how to use it with Laminar and Kernel: Stagehand v3 requires `@lmnr-ai/lmnr@0.7.11` or later. ```javascript Stagehand v3 theme={null} import { Laminar, observe } from '@lmnr-ai/lmnr'; import { Stagehand } from '@browserbasehq/stagehand'; import Kernel from '@onkernel/sdk'; import { z } from 'zod'; // Initialize Laminar with Stagehand instrumentation Laminar.initialize({ projectApiKey: process.env.LMNR_PROJECT_API_KEY, instrumentModules: { stagehand: Stagehand, kernel: Kernel, }, }); // Initialize Kernel and create a browser const kernel = new Kernel(); const main = async () => observe({ name: 'main' }, async () => { // Create browser const kernelBrowser = await kernel.browsers.create({ stealth: true, }); console.log("Live view url:", kernelBrowser.browser_live_view_url); // Configure Stagehand to use Kernel's browser const stagehand = new Stagehand({ env: "LOCAL", localBrowserLaunchOptions: { cdpUrl: kernelBrowser.cdp_ws_url, }, model: "openai/gpt-4.1", apiKey: process.env.OPENAI_API_KEY, verbose: 1, domSettleTimeout: 30_000 }); await stagehand.init(); // Your automation code const page = stagehand.context.pages()[0]; if (!page) { throw new Error('No page available'); } await page.goto("https://www.ycombinator.com/companies"); await stagehand.act("Type in Kernel into the search box"); await stagehand.act("Click on the first search result"); // Extract team size from the YC startup page const output = await stagehand.extract( "Extract the team size (number of employees) shown on this Y Combinator company page.", z.object({ teamSize: z.string(), }) ); console.log("Team size:", output.teamSize); // Cleanup await stagehand.close(); await kernel.browsers.deleteByID(kernelBrowser.session_id); }); await main(); await new Promise(resolve => setTimeout(resolve, 5000)); await Laminar.flush(); ``` ```javascript Stagehand v2 theme={null} import { Laminar, observe } from '@lmnr-ai/lmnr'; import { Stagehand } from '@browserbasehq/stagehand'; import Kernel from '@onkernel/sdk'; import { z } from 'zod'; // Initialize Laminar with Stagehand instrumentation Laminar.initialize({ projectApiKey: process.env.LMNR_PROJECT_API_KEY, instrumentModules: { stagehand: Stagehand, kernel: Kernel, }, }); // Initialize Kernel and create a browser const kernel = new Kernel(); const main = async () => observe({ name: 'main' }, async () => { const kernelBrowser = await kernel.browsers.create({ stealth: true }); console.log("Live view url:", kernelBrowser.browser_live_view_url); // Configure Stagehand to use Kernel's browser const stagehand = new Stagehand({ env: 'LOCAL', localBrowserLaunchOptions: { cdpUrl: kernelBrowser.cdp_ws_url } verbose: 1, domSettleTimeoutMs: 30_000, modelName: 'openai/gpt-4.1', modelClientOptions: { apiKey: process.env.OPENAI_API_KEY } }); await stagehand.init(); // Your automation code const page = stagehand.page; await page.goto('https://www.onkernel.com/docs'); // Navigate to careers page await page.goto('https://www.onkernel.com/docs/careers/intro'); // Extract all job URLs const output = await page.extract({ instruction: 'Extract all job posting URLs from the Open Roles section.', schema: z.object({ jobUrls: z.array(z.string()).describe('Array of job posting URLs') }) }); console.log('Job URLs found:', output.jobUrls); console.log(`Total jobs: ${output.jobUrls.length}`); // Clean up and flush traces to Laminar await stagehand.close(); // Delete the browser for those who left open the live view url await kernel.browsers.deleteByID(kernelBrowser.session_id); }); await main(); await new Promise(resolve => setTimeout(resolve, 5000)); await Laminar.flush(); ``` ## Tracing Kernel Apps & Computer Controls When you use Kernel's [App platform](/docs/apps/develop) or [Computer controls](/docs/browsers/computer-controls), Laminar will automatically trace the computer and process interactions. In addition, you don't have to manually `observe` your kernel `app.actions` or worry about manual trace flushing inside your Kernel apps. Laminar will take care of trace lifecycle automatically for Kernel apps. ### Example Kernel app with Laminar tracing To deploy this example on Kernel, follow the steps in [Kernel's app deployment guide](/docs/apps/deploy). ```javascript Typescript/Javascript theme={null} import { Laminar } from '@lmnr-ai/lmnr'; import { chromium } from 'playwright'; import { config } from 'dotenv'; import Kernel, { type KernelContext } from "@onkernel/sdk"; config(); Laminar.initialize({ instrumentModules: { playwright: { chromium, }, kernel: Kernel, } }); const kernel = new Kernel(); const app = kernel.app('my-app-name'); app.action('my-action', async (ctx: KernelContext, payload) => { const kernelBrowser = await kernel.browsers.create({ invocation_id: ctx.invocation_id, }); 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()); // Your automation code await page.goto('https://www.duckduckgo.com/') await page.waitForTimeout(2000) await page.goto('https://www.github.com/trending'); // Wait for 2 seconds await page.waitForTimeout(2000); // Extract all trending repos const trendingRepos = await page.locator("h2 a.Link").evaluateAll((repos) => repos.map((repo) => repo.textContent.trim().replace(/[\n\s]/g, '')) ); console.log('Trending repos:', trendingRepos); await page.waitForTimeout(2000); // Computer tool call await kernel.browsers.computer.captureScreenshot( kernelBrowser.session_id, ); // Process tool call await kernel.browsers.process.exec( kernelBrowser.session_id, { command: 'ls', args: ['-la'], } ) await browser.close(); await kernel.browsers.deleteByID(kernelBrowser.session_id); return { success: true, result: "Action completed successfully" }; }); ``` ```python Python theme={null} from lmnr import Laminar from playwright.async_api import async_playwright from kernel import App, Kernel, KernelContext # Initialize Laminar Laminar.initialize() kernel_client = Kernel() app = App("my-app-name") @app.action(name="my-action") async def run_automation(ctx: KernelContext): # Connect Playwright to Kernel's browser async with async_playwright() as p: kernel_browser = kernel_client.browsers.create( invocation_id=ctx.invocation_id ) browser = await p.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() # Your automation code await page.goto('https://www.duckduckgo.com/') await page.wait_for_timeout(2000) await page.goto('https://www.github.com/trending') await page.wait_for_timeout(2000) # Extract all trending repos trending_repos = await page.locator("h2 a.Link").evaluate_all(r""" (repos) => repos.map((repo) => repo.textContent.trim().replace(/[\n\s]/g, '') ) """) print('Trending repos:', trending_repos) # Process tool call kernel_client.browsers.process.exec( id=kernel_browser.session_id, command="ls", args=["-la"], ) # Computer tool call kernel_client.browsers.computer.move_mouse( id=kernel_browser.session_id, x=100, y=100, ) # Wait for 3 seconds await page.wait_for_timeout(3000) # Clean up the browser await browser.close() # Delete the browser for those who left open the Kernel live view url kernel_client.browsers.delete_by_id(kernel_browser.session_id) print("Done") ``` ## Viewing traces in Laminar View your traces in the Laminar UI's traces tab to see synchronized browser session recordings and agent execution steps. After running your automation: 1. Log in to your [Laminar dashboard](https://www.lmnr.ai/) 2. Navigate to the **Traces** tab 3. Find your recent trace to view: * Full execution timeline * LLM calls and responses * Browser session recordings * Computer and process interactions (for Kernel apps) * Token usage and costs * Latency metrics Timeline highlights indicate which step your agent is currently executing, making it easy to debug and optimize your automations. ## Next steps * Explore [Laminar's tracing structure](https://docs.lmnr.ai/tracing/structure/overview) to understand how traces are organized * Learn about [Laminar's evaluations](https://docs.lmnr.ai/evaluations/introduction) for validating and testing your AI application outputs * Learn about [stealth mode](/docs/browsers/bot-detection/stealth) for avoiding detection * Learn how to [deploy your app](/docs/apps/deploy) to Kernel's platform # Overview Source: https://kernel.sh/docs/integrations/overview Kernel's browsers are compatible with all browser and Computer Use frameworks. ## Universal CDP compatibility Kernel browsers work with any framework or tool that supports the Chrome DevTools Protocol (CDP). This means you can: * **Use any agent framework**: Integrate with popular frameworks like Browser Use, Stagehand, Playwright, Puppeteer, Selenium, and more * **Connect via CDP**: All browsers expose a CDP WebSocket URL for direct connection * **No vendor lock-in**: Switch between frameworks or use multiple frameworks simultaneously * **Standard protocols**: Built on open standards that work with the entire browser automation ecosystem ## Computer Use For vision-language models (VLMs) that predict browser actions from screenshots, Kernel provides [Computer Controls APIs](/docs/browsers/computer-controls) that enable direct mouse, keyboard, and screen interactions. These low-level controls let you: * Capture screenshots to send to your VLM * Execute predicted actions (clicks, typing, scrolling, dragging) * Build custom agentic loops with any VLM provider This approach works with any computer use model, including Anthropic Claude, OpenAI CUA, Google Gemini, and others. ## Popular Framework Integrations Kernel provides detailed guides for popular agent frameworks: * **[Agent Browser](/docs/integrations/vercel/agent-browser)** - Browser automation CLI for AI agents * **[fx](/docs/integrations/vercel/fx)** - Give Vercel's fx coding agent a Kernel cloud browser via MCP * **[Browser Use](/docs/integrations/browser-use)** - AI browser agent framework * **[Hermes Agent](/docs/integrations/hermes-agent)** - Run Hermes browser tools on Kernel cloud browsers * **[Claude Code and Desktop](/docs/integrations/claude/claude-code-and-desktop)** - Give the Claude apps a Kernel browser via the marketplace plugin or MCP * **[Claude Agent SDK](/docs/integrations/claude/claude-agent-sdk)** - Run Claude Agent SDK automations in cloud browsers * **[Claude Managed Agents](/docs/integrations/claude/claude-managed-agents)** - Run Anthropic's hosted agent harness against cloud browsers * **[Stagehand](/docs/integrations/stagehand)** - AI browser automation with natural language * **[Terraform](/docs/integrations/terraform)** - Manage durable Kernel infrastructure as code * **[Computer Use (Anthropic)](/docs/integrations/computer-use/anthropic)** - Claude's computer use capability * **[Computer Use (OpenAI)](/docs/integrations/computer-use/openai)** - OpenAI's computer use capability * **[Computer Use (Gemini)](/docs/integrations/computer-use/gemini)** - Gemini's computer use capability * **[Computer Use (OpenAGI)](/docs/integrations/computer-use/openagi)** - OpenAGI's computer use capability * **[Computer Use (Yutori)](/docs/integrations/computer-use/yutori)** - Yutori Navigator n1.5 pixels-to-actions model * **[Laminar](/docs/integrations/laminar)** - Observability and tracing for AI browser automations * **[Payments](/docs/integrations/payments/overview)** - Add provider-backed payments to browser agents without exposing card data * **[Val Town](/docs/integrations/valtown)** - Serverless function runtime * **[Stripe Projects](/docs/integrations/stripe-projects)** - Provision Kernel plans and API keys via the Stripe Projects CLI * **[Vercel](https://github.com/onkernel/vercel-template)** - Deploy browser automations to Vercel * **[Web Bot Authentication](/docs/browsers/bot-detection/web-bot-auth)** - Create signed Chrome extensions for web bot authentication * **[1Password](/docs/integrations/1password)** - Use credentials from your 1Password vaults for Managed Auth ## Custom Integrations Kernel works with any tool that supports CDP. Check out our [browser control guide](/docs/introduction/control) to learn how to connect any other agent framework. # Agentcard Source: https://kernel.sh/docs/integrations/payments/agentcard Use Agentcard to approve browser checkouts against an enrolled payment method [agentcard](https://www.agentcard.sh/) collects a user's card in a hosted enrollment flow and authorizes each browser checkout against that enrolled payment method. the card number and cvc stay with agentcard. your agent receives non-secret aliases. agentcard is the credential provider, not the merchant's payment processor. at the browser form layer, it works with any web checkout that accepts standard card details, and the merchant's processor doesn't need to be stripe. end-to-end handoff also requires the outgoing request to match a [native processor adapter](/docs/integrations/payments/overview#checkout-and-processor-coverage). KERNEL currently has adapters for request formats used by stripe, shopify, square, recurly, and razorpay. KERNEL's native handoff aims to support the same processors supported by agentcard's direct SDK. email [support@kernel.sh](mailto:support@kernel.sh) if you need another processor so we can prioritize its adapter and validate a real checkout. ## Before you start create a project-scoped client and vault. the examples below use these `kernel` and `vault` variables. ```typescript TypeScript theme={null} import Kernel from "@onkernel/sdk"; const kernel = new Kernel({ projectID: process.env.KERNEL_PROJECT_ID! }); const vault = await kernel.vaults.upsert({ name: "user-12345" }); const agentcardMode = process.env.AGENTCARD_MODE; if (agentcardMode !== "sandbox" && agentcardMode !== "live") { throw new Error("set AGENTCARD_MODE to sandbox or live"); } ``` ```python Python theme={null} import os from kernel import Kernel kernel = Kernel(project_id=os.environ["KERNEL_PROJECT_ID"]) vault = kernel.vaults.upsert(name="user-12345") agentcard_mode = os.environ.get("AGENTCARD_MODE") if agentcard_mode not in {"sandbox", "live"}: raise RuntimeError("set AGENTCARD_MODE to sandbox or live") ``` ```bash CLI theme={null} export AGENTCARD_MODE=live kernel vaults create --name user-12345 ``` the vault api does not expose whether the configured agentcard credential is sandbox or live. `AGENTCARD_MODE` is an application-owned assertion, not a value read from KERNEL. set it from the deployment configuration that owns the agentcard credential, show the mode in internal checkout controls, and fail closed when it is missing or does not match the environment you intend to use. set `AGENTCARD_MODE=sandbox` instead only when that deployment uses a sandbox credential. ## Lifecycle 1. create a `wallet` item and open the returned `card_enrollment` action for the user. 2. wait for the wallet to become `connected`. 3. create a reusable `card` item with the merchant, amount, and currency. 4. attach the vault to a browser and give `state.aliases` to the agent. 5. when the browser submits a recognized processor request containing the aliases, KERNEL holds the request and starts agentcard authorization. 6. show the returned approval action to the user while the checkout remains in progress. 7. agentcard executes the approved request, and KERNEL replays the processor response to the browser. the card item returns to `ready` after an authorization settles and can be used for another separately approved purchase. only one authorization can be pending on an item at a time. ## Enroll a card before showing an agentcard enrollment option, list the vault's items. if an agentcard wallet already exists in any state, reuse it and do not let the user add another. show its existing action or status instead. the api makes item keys unique but does not currently enforce one wallet per provider, so the ui must enforce a maximum of one agentcard wallet per vault. the examples use `presentProviderAction`, an application-owned function that publishes the action to an authenticated session for the end user who owns the vault. bind the action to that user, vault, and item; apply a short application ttl capped by `wallet.expires_at` when present; and stop serving it when the action changes or disappears. derive `authenticatedUser` from the server-side session, not a request field. do not log the url or put it in model context. the [browser payment guide](/docs/browsers/enable-payments-in-browser-agent#present-hosted-actions-in-your-application) defines the authenticated redirect and expiry contract for this helper. ```typescript TypeScript theme={null} const items = await kernel.vaults.items.list(vault.id); const agentcardWallets = items.filter( (item) => item.type === "wallet" && item.spec.provider === "agentcard", ); if (agentcardWallets.length > 1) { throw new Error("vault has more than one agentcard wallet"); } let wallet = agentcardWallets[0]; if (!wallet) { wallet = await kernel.vaults.items.upsert("agentcard-wallet", { id_or_name: vault.id, type: "wallet", spec: { provider: "agentcard" }, }); } if (wallet.action?.name === "card_enrollment") { await presentProviderAction({ userID: authenticatedUser.id, vaultID: vault.id, item: wallet, }); } wallet = await kernel.vaults.items.retrieve(wallet.key, { id_or_name: vault.id, wait: 60, }); ``` ```python Python theme={null} items = kernel.vaults.items.list(vault.id) agentcard_wallets = [ item for item in items if item.type == "wallet" and item.spec.provider == "agentcard" ] if len(agentcard_wallets) > 1: raise RuntimeError("vault has more than one agentcard wallet") wallet = agentcard_wallets[0] if agentcard_wallets else None if wallet is None: wallet = kernel.vaults.items.upsert( "agentcard-wallet", id_or_name=vault.id, type="wallet", spec={"provider": "agentcard"}, ) if wallet.action is not None and wallet.action.name == "card_enrollment": present_provider_action( user_id=authenticated_user.id, vault_id=vault.id, item=wallet, ) wallet = kernel.vaults.items.retrieve( wallet.key, id_or_name=vault.id, wait=60, ) ``` ```bash CLI theme={null} # create only when the list has no agentcard wallet kernel vaults items list user-12345 -o json kernel vaults wallets create user-12345 agentcard-wallet \ --provider agentcard \ --spec '{}' \ --open kernel vaults items get user-12345 agentcard-wallet --wait 60 ``` open the enrollment url in a trusted user-facing surface. don't give it to the agent or open it in the agent-controlled checkout browser. run cli `--open` only from a trusted, human-operated terminal because the command output can contain the action url. `spec.user_id` can reuse a user who was already enrolled through another wallet in your organization. it cannot reference an arbitrary agentcard user. ## Create a card item ```typescript TypeScript theme={null} const card = await kernel.vaults.items.upsert("notebook-order", { id_or_name: vault.id, type: "card", spec: { provider: "agentcard", wallet: wallet.key, merchant: "example shop", amount: 2306, currency: "usd", }, }); if (card.state.status !== "ready" || !card.state.aliases) { throw new Error(`card is ${card.state.status}`); } ``` ```python Python theme={null} card = kernel.vaults.items.upsert( "notebook-order", id_or_name=vault.id, type="card", spec={ "provider": "agentcard", "wallet": wallet.key, "merchant": "example shop", "amount": 2306, "currency": "usd", }, ) if card.state.status != "ready" or card.state.aliases is None: raise RuntimeError(f"card is {card.state.status}") ``` ```bash CLI theme={null} kernel vaults cards create user-12345 notebook-order \ --provider agentcard \ --spec '{ "wallet": "agentcard-wallet", "merchant": "example shop", "amount": 2306, "currency": "usd" }' kernel vaults items get user-12345 notebook-order -o json ``` `amount` uses minor currency units, so `2306` means 23.06 usd. omitting `card_id` lets the cardholder select an enrolled card on the approval screen. to pin a card, request the wallet's advertised `payment_methods` expansion and set a returned id as `spec.card_id`. ### Reuse a card item for a new purchase `upsert` can retrieve an identical item, but it cannot replace the purchase specification at an existing key. for a later purchase, retrieve the reusable agentcard item and use `update` with the complete new specification: ```typescript TypeScript theme={null} let reusableCard = await kernel.vaults.items.retrieve("notebook-order", { id_or_name: vault.id, wait: 60, }); if ( reusableCard.type !== "card" || reusableCard.spec.provider !== "agentcard" || (reusableCard.state.status !== "requested" && reusableCard.state.status !== "ready") ) { throw new Error(`card cannot be updated from ${reusableCard.state.status}`); } reusableCard = await kernel.vaults.items.update("notebook-order", { id_or_name: vault.id, spec: { provider: "agentcard", wallet: wallet.key, merchant: "example books", amount: 4199, currency: "usd", }, }); ``` ```python Python theme={null} reusable_card = kernel.vaults.items.retrieve( "notebook-order", id_or_name=vault.id, wait=60, ) if ( reusable_card.type != "card" or reusable_card.spec.provider != "agentcard" or reusable_card.state.status not in {"requested", "ready"} ): raise RuntimeError( f"card cannot be updated from {reusable_card.state.status}" ) reusable_card = kernel.vaults.items.update( "notebook-order", id_or_name=vault.id, spec={ "provider": "agentcard", "wallet": wallet.key, "merchant": "example books", "amount": 4199, "currency": "usd", }, ) ``` ```bash CLI theme={null} kernel vaults items get user-12345 notebook-order --wait 60 -o json kernel vaults cards update user-12345 notebook-order \ --provider agentcard \ --spec '{ "wallet": "agentcard-wallet", "merchant": "example books", "amount": 4199, "currency": "usd" }' ``` the api accepts an agentcard card update only while the item is `requested` or `ready`. if it is `pending_approval`, finish and reconcile that authorization before preparing another purchase. if it is `degraded`, retrieve it to allow recovery and stop if it remains degraded. `update` replaces the full `spec`, so include `card_id` again when you want to keep the card pinned. never update an item to retry a failed, timed-out, or indeterminate checkout. agentcard has no per-item `test`, `merchant_url`, or domain allowlist. `merchant` is the name shown on the approval screen, not an enforced browsing origin. sandbox or live behavior comes from the agentcard credential configured for the integration and must match your application-owned `AGENTCARD_MODE` assertion before you use the aliases. ## Complete the first checkout use this sequence for an agentcard checkout: 1. require exactly one agentcard wallet in the vault and wait for it to become `connected`. 2. create a headful browser with the vault attached, surface `browser_live_view_url` through your trusted application, and navigate to the checkout. keep this same browser for verification and submission so location-dependent pricing cannot change between the confirmed purchase and the outgoing request. 3. independently verify the merchant, items, active presentment amount, and active presentment currency from the merchant's trusted order or cart backend. if one isn't available, use documented structured checkout data or deterministic extraction for that checkout. for a stripe payment link specifically, prefer `account_settings.display_name`, `line_item_group.total`, `line_item_group.currency`, and `line_item_group.line_items` from the structured payment-link response. use dom text and test ids only as supplemental checks because stripe can duplicate or omit them across layouts. 4. collect merchant-required fields such as email, billing name, and postal code from the end user. identify checkout-specific agent disclosures and instruct the agent to answer them truthfully in the normal form. 5. show the verified purchase to the end user. after confirmation, create or update the card item from that same frozen object and require it to be `ready`. the vault attachment covers items created later in the same vault. 6. start the card and event observer before checkout submission. keep it running concurrently while the browser request is held. 7. give the browser agent the aliases, separately collected customer fields, and any required disclosure answer. submit the merchant form once and never retry submission. 8. publish the approval action through the authenticated, expiring application flow. never send it to the checkout browser or agent. 9. after the authorization settles, reconcile authorization state, item events, the checkout page, and the merchant order record. don't prepare the next purchase until this attempt is terminal or explicitly classified as indeterminate. ### Surface the live view ```typescript TypeScript theme={null} const browser = await kernel.browsers.create({ vaults: [{ id: vault.id }], headless: false, timeout_seconds: 1800, }); if (!browser.browser_live_view_url) { throw new Error("headful browser did not return a live view url"); } await presentLiveView({ userID: authenticatedUser.id, sessionID: browser.session_id, url: browser.browser_live_view_url, }); ``` ```python Python theme={null} browser = kernel.browsers.create( vaults=[{"id": vault.id}], headless=False, timeout_seconds=1800, ) if browser.browser_live_view_url is None: raise RuntimeError("headful browser did not return a live view url") present_live_view( user_id=authenticated_user.id, session_id=browser.session_id, url=browser.browser_live_view_url, ) ``` ```bash CLI theme={null} kernel browsers create --vault user-12345 -o json ``` `presentLiveView` represents an application-owned route. store the url server-side with the authenticated end user and browser session binding, and render or embed it only after checking that session. retain the same binding through confirmation and approval pauses. remove it when the browser is deleted or times out, and don't put it in logs or model context. see [live view](/docs/browsers/live-view#embedding-in-an-iframe) for iframe and csp requirements. ### Run checkout and observation concurrently start the observer before calling the browser agent. the application-owned functions below represent the observer, your existing agent loop, your order backend, and your reconciliation policy: ```typescript TypeScript theme={null} const stop = new AbortController(); const observer = observePayment(stop.signal); try { const pageOutcome = await runBrowserAgentCheckout({ browser, aliases: card.state.aliases, verifiedPurchase, customerFields, disclosure: "I am an AI agent acting on behalf of someone else", }); const merchantOrder = await waitForMerchantResolution( verifiedPurchase.orderID, ); const current = await kernel.vaults.items.retrieve(card.key, { id_or_name: vault.id, }); if (current.type !== "card" || current.state.provider !== "agentcard") { throw new Error("expected an agentcard card item"); } const events = await kernel.vaults.items.events(card.key, { id_or_name: vault.id, }); const result = reconcileAgentcard({ verifiedPurchase, merchantOrder, authorization: current.state.authorization, events, pageOutcome, }); handleReconciledResult(result); } finally { stop.abort(); try { await observer; } finally { await kernel.browsers.deleteByID(browser.session_id); } } ``` ```python Python theme={null} from threading import Event, Thread stop = Event() observer = Thread(target=observe_payment, args=(stop,)) observer.start() try: page_outcome = run_browser_agent_checkout( browser=browser, aliases=card.state.aliases, verified_purchase=verified_purchase, customer_fields=customer_fields, disclosure="I am an AI agent acting on behalf of someone else", ) merchant_order = wait_for_merchant_resolution(verified_purchase.order_id) current = kernel.vaults.items.retrieve(card.key, id_or_name=vault.id) if current.type != "card" or current.state.provider != "agentcard": raise RuntimeError("expected an agentcard card item") events = kernel.vaults.items.events(card.key, id_or_name=vault.id) result = reconcile_agentcard( verified_purchase=verified_purchase, merchant_order=merchant_order, authorization=current.state.authorization, events=events, page_outcome=page_outcome, ) handle_reconciled_result(result) finally: stop.set() try: observer.join() finally: kernel.browsers.delete_by_id(browser.session_id) ``` the observer must have its own terminal-state loop and application deadline. cancel it after the merchant reaches a terminal state or the deadline expires. if the deadline expires, retain the vault id, card key, browser id, and last event id, classify the attempt as indeterminate, and don't resubmit checkout. ## Handle checkout approval agentcard doesn't advertise the `authorize` operation. authorization begins only after an attached browser submits a recognized processor request containing the aliases. while the request is held, retrieve the card and send `action.url` through the same authenticated, expiring user-action flow used for enrollment. stop serving the url when it disappears, changes, expires, or the authorization settles. `state.authorization` describes the pending or most recent authorization, including its `status`, `browser_id`, expected and actual amounts when available, charge result, and replay result. ```bash CLI theme={null} kernel vaults items get user-12345 notebook-order --wait 60 --open kernel vaults items events user-12345 notebook-order --wait 60 -o json ``` each `--wait` performs one bounded observation. repeat the relevant command to continue observing an existing checkout; don't use it as evidence that a payment succeeded or failed. run `--open` only in a trusted, human-operated terminal and never pass its output to an agent. | field | interpretation | | ---------------------------------- | --------------------------------------------------------------------------------------- | | `amount_authority: 'display_only'` | the request does not prove a charge amount; tokenization requests can have this value | | `amount_verified` | whether the observed amount matched the approved amount when verification was available | | `charged_kind` | `captured`, `authorized`, or `none` as reported by the provider path | | `replay_attempted` | whether KERNEL attempted to replay the processor response | | `replay_delivered` | whether that response reached the browser; this does not confirm a merchant order | declines, expirations, and provider failures can return a processor-shaped failure response to the browser. the exact response depends on the native adapter. the reusable item can still return to `ready`, so item status alone doesn't prove that the purchase succeeded or failed. your reconciliation policy must return `succeeded` only when the merchant order record confirms a paid order whose merchant, items, amount, and currency match the frozen purchase object. treat `state.authorization`, `charged_kind`, `replay_delivered`, item events, and the checkout page as supporting evidence. merchant success text is page-specific: **Thanks for your payment** can appear for one stripe checkout, but no generic success-text matcher proves that the merchant created the expected order. return `indeterminate` when the sources disagree or the merchant record is unavailable, and don't retry automatically. pass the aliases to the [browser agent payments guide](/docs/browsers/enable-payments-in-browser-agent) and observe item events while the checkout runs. # overview Source: https://kernel.sh/docs/integrations/payments/overview Add payments to a browser agent without exposing card data to your application, agent, or browser your browser agent can complete a web checkout without bringing your application, agent, or browser into pci dss scope. a provider-hosted flow collects and stores the user's payment method, so neither you nor your agent handles the card number or cvc. KERNEL connects that payment method to a [vault](/docs/vaults), returns non-secret aliases, and resolves those aliases at browser egress. the agent fills the checkout form with the aliases. the merchant page creates its normal payment request. KERNEL handles authorization and payment handoff outside the browser. stripe link and agentcard are credential providers, not merchant payment processors. at the browser form layer, both work with any web checkout that accepts standard card details, and the merchant's processor does not need to be stripe. end-to-end handoff also requires the outgoing payment request to match a native KERNEL processor adapter. ## How payments work both credential providers use the same integration shape: 1. create a vault for the user or task. 2. create a wallet item and send the user through the provider-hosted collection flow. 3. create a card item for the intended purchase and wait for aliases. 4. attach the vault when you create the browser session. 5. give the aliases to the agent and let it complete the merchant's checkout. 6. complete any provider-hosted approval and inspect item events alongside the merchant's order state. each vault must contain at most one wallet item for each provider. before showing a provider connection option, list the vault's items. if that provider already has a wallet in any state, hide the add option and reuse or recover the existing item. when both stripe link and agentcard wallets exist, show both as configured and do not offer either provider again. ```mermaid theme={null} flowchart LR U[user] --> H[credential-provider collection] H --> W[wallet item] W --> C[card item and aliases] C --> A[agent fills checkout] A --> E[KERNEL egress] E --> P[merchant processor] P --> M[merchant response] ``` the card number and cvc stay outside the agent-controlled environment. the browser sees format-valid aliases and the processor-shaped response, not the underlying payment credential. ## Choose a provider collect a stripe link wallet and approve a one-use credential for a specific purchase. add users' cards to agentcard's vault. the user approves each transaction with Face ID and earns points on the purchase. stripe link and agentcard identify where the credential comes from and how the user approves it. choose between them based on that lifecycle, not the merchant processor. processor-adapter coverage is the same for both. | behavior | stripe link | agentcard | | ------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------ | | payment-method collection | hosted `link_oauth` action | fully white-labeled `card_enrollment` page | | purchase authorization | explicit `authorize` operation before checkout | the user approves with Face ID | | payment handoff | one-use credential substituted at egress | agentcard executes the request and returns the response | | reuse | card item and aliases are consumed on first substitution | cards can be reused for recurring and one-time purchases | | environment | live only | configured agentcard credential; not exposed through the vault api | choose [stripe link](https://hypeship.dev/integrations/payments/stripe-link) when each purchase requires a newly approved, single-use credential. choose [agentcard](https://hypeship.dev/integrations/payments/agentcard) when one enrolled card must support multiple purchases, with separate approval for each. both integrations may provide additional benefits, including card rewards and chargeback protection. review each provider’s own documentation for the most up-to-date details. ## Checkout and processor coverage KERNEL currently includes native adapters for these checkout request formats. all five adapters are enabled for both stripe link and agentcard. | merchant processor or platform | recognized HTTPS `POST` request formats | | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- | | stripe | form requests to `api.stripe.com/v1/payment_methods`, `/v1/tokens`, `/v1/payment_intents/{id}/confirm`, and `/v1/payment_pages/{id}/confirm` | | shopify | JSON card-session requests to `checkout.pci.shopifyinc.com/sessions` and `deposit..shopifycs.com/sessions` | | square | JSON card-nonce requests to `pci-connect.squareup.com/v2/card-nonce` and `pci-connect.squareupsandbox.com/v2/card-nonce` | | recurly | form token requests to `api.recurly.com/js/v1/token` and `api.eu.recurly.com/js/v1/token` | | razorpay | form card-payment requests to `api.razorpay.com/v1/payments/create/ajax` and `api.razorpay.com/v1/standard_checkout/payments/create/ajax` | for example, a browser can enter stripe link or agentcard aliases into a Shopify checkout. Shopify remains the merchant platform; stripe link or agentcard supplies the credential and approval flow. the outgoing request must contain the complete alias set and match the adapter's expected HTTPS method, host, path, content type, and card-field layout. these adapters are enabled today, but non-Stripe coverage still needs broader validation against real processor SDKs and hosted checkouts. encrypted payloads, different request layouts, and unrecognized processor endpoints pass through without native handoff. KERNEL's native handoff aims to support the same processors supported by agentcard's direct SDK. email [support@kernel.sh](mailto:support@kernel.sh) if you need another processor so we can prioritize its adapter and validate a real checkout. ## Why use KERNEL handoff * integrate with one KERNEL vault api for both stripe link and agentcard. * let KERNEL intercept recognized payment requests at egress, including requests from embedded payment frames, instead of maintaining cdp interception or page-routing logic in your agent. * verify the project, browser session, vault attachment, item, aliases, and lifecycle state before handoff, then record the payment lifecycle in vault audit events. * keep the underlying credential outside your application, agent, and browser through aliases and provider-hosted enrollment and approval. the handoff is designed to prevent credential injection and browser-to-provider handoff failures. we don't yet have comparative data showing higher checkout completion or payment acceptance rates. expanding acceptance coverage against real processors and measuring reliability are active priorities. don't retry a failed, timed-out, rejected, or indeterminate payment. a browser error, missing response, consumed link item, or reusable agentcard item does not prove whether the merchant created an order or money moved. inspect the existing item events and the merchant's order state before taking another action. ## Next step configure [stripe link](/docs/integrations/payments/stripe-link) or [agentcard](/docs/integrations/payments/agentcard), then follow [Enable Payments in a Browser Agent](/docs/browsers/enable-payments-in-browser-agent) to attach the vault and give payment aliases to your agent. the provider pages show the CLI commands for creating wallets and cards. once the card item is ready, the shared CLI flow is: ```bash CLI theme={null} kernel vaults create --name user-12345 kernel vaults items get user-12345 notebook-order --wait 60 -o json kernel browsers create --vault user-12345 -o json ``` `--wait` performs one bounded observation. it does not confirm that a payment succeeded, and the CLI does not submit or retry merchant payments. # stripe link Source: https://kernel.sh/docs/integrations/payments/stripe-link Use stripe link to approve a one-use payment credential for a browser checkout [stripe link](https://stripe.com/payments/link) connects a user's wallet through oauth and issues a one-use payment credential for an approved purchase. KERNEL stores that credential encrypted, gives your agent non-secret aliases, substitutes the credential at browser egress, and then consumes the card item. stripe link is the credential provider, not the merchant's payment processor. at the browser form layer, it works with any web checkout that accepts standard card details, and the merchant's processor doesn't need to be stripe. end-to-end handoff also requires the outgoing request to match a [native processor adapter](/docs/integrations/payments/overview#checkout-and-processor-coverage). for example, you can use a stripe link credential in a Shopify checkout. ## Before you start create a project-scoped client and vault. the examples below use these `kernel` and `vault` variables. ```typescript TypeScript theme={null} import Kernel from "@onkernel/sdk"; const kernel = new Kernel({ projectID: process.env.KERNEL_PROJECT_ID! }); const vault = await kernel.vaults.upsert({ name: "user-12345" }); ``` ```python Python theme={null} import os from kernel import Kernel kernel = Kernel(project_id=os.environ["KERNEL_PROJECT_ID"]) vault = kernel.vaults.upsert(name="user-12345") ``` ```bash CLI theme={null} kernel vaults create --name user-12345 ``` ## Lifecycle 1. create a `wallet` item with the link oauth specification. 2. open the returned `link_oauth` action for the user and wait for the wallet to become `connected`. 3. request the advertised `payment_methods` expansion and let the user choose an eligible method. 4. create a `card` item with the purchase details. 5. retrieve the card, verify that it advertises `authorize`, and perform that operation after explicit user approval. 6. complete the returned `spend_approval` or `push_approval` action and wait for `state.status` to become `ready`. 7. use `state.aliases` in an attached browser. the first native handoff changes the item to `consumed`. ## Connect a wallet before showing a stripe link connection option, list the vault's items. if a link wallet already exists in any state, reuse it and do not let the user add another. show its existing action or status instead. the api makes item keys unique but does not currently enforce one wallet per provider, so the ui must enforce a maximum of one link wallet per vault. the examples use `presentProviderAction`, an application-owned function that publishes the action to an authenticated session for the end user who owns the vault. bind the action to that user, vault, and item; apply a short application ttl capped by `wallet.expires_at` when present; and stop serving it when the action changes or disappears. derive `authenticatedUser` from the server-side session, not a request field. do not log the url or put it in model context. ```typescript TypeScript theme={null} const items = await kernel.vaults.items.list(vault.id); const linkWallets = items.filter( (item) => item.type === "wallet" && item.spec.provider === "link", ); if (linkWallets.length > 1) { throw new Error("vault has more than one link wallet"); } let wallet = linkWallets[0]; if (!wallet) { wallet = await kernel.vaults.items.upsert("link-wallet", { id_or_name: vault.id, type: "wallet", spec: { provider: "link", authorization: { method: "oauth", client: { type: "kernel_managed" }, }, }, }); } if (wallet.action?.name === "link_oauth") { await presentProviderAction({ userID: authenticatedUser.id, vaultID: vault.id, item: wallet, }); } wallet = await kernel.vaults.items.retrieve(wallet.key, { id_or_name: vault.id, wait: 60, }); ``` ```python Python theme={null} items = kernel.vaults.items.list(vault.id) link_wallets = [ item for item in items if item.type == "wallet" and item.spec.provider == "link" ] if len(link_wallets) > 1: raise RuntimeError("vault has more than one link wallet") wallet = link_wallets[0] if link_wallets else None if wallet is None: wallet = kernel.vaults.items.upsert( "link-wallet", id_or_name=vault.id, type="wallet", spec={ "provider": "link", "authorization": { "method": "oauth", "client": {"type": "kernel_managed"}, }, }, ) if wallet.action is not None and wallet.action.name == "link_oauth": present_provider_action( user_id=authenticated_user.id, vault_id=vault.id, item=wallet, ) wallet = kernel.vaults.items.retrieve( wallet.key, id_or_name=vault.id, wait=60, ) ``` ```bash CLI theme={null} # create only when the list has no link wallet kernel vaults items list user-12345 -o json kernel vaults wallets create user-12345 link-wallet \ --provider link \ --spec '{"authorization":{"method":"oauth","client":{"type":"kernel_managed"}}}' \ --open kernel vaults items get user-12345 link-wallet --wait 60 ``` open collection and approval urls in a trusted user-facing surface. don't give them to the agent or print full wallet responses into model context. run cli `--open` only from a trusted, human-operated terminal because the command output can contain the action url. ## Select a payment method request `payment_methods` only when `available_expansions` advertises it. the expansion comes from link at request time and is not persisted in the vault item. ```typescript TypeScript theme={null} if ( !wallet.available_expansions.some(({ type }) => type === "payment_methods") ) { throw new Error("payment methods are unavailable"); } wallet = await kernel.vaults.items.retrieve(wallet.key, { id_or_name: vault.id, expand: ["payment_methods"], }); const methods = wallet.expanded?.payment_methods ?? []; for (const method of methods) { console.log(method.id, method.display, method.capabilities); } const paymentMethod = methods.find( ({ id }) => id === process.env.LINK_PAYMENT_METHOD_ID, ); if ( !paymentMethod || paymentMethod.capabilities.single_use_card?.eligible === false ) { throw new Error("select an available payment method"); } ``` ```python Python theme={null} if not any(expansion.type == "payment_methods" for expansion in wallet.available_expansions): raise RuntimeError("payment methods are unavailable") wallet = kernel.vaults.items.retrieve( wallet.key, id_or_name=vault.id, expand=["payment_methods"], ) methods = (wallet.expanded.payment_methods or []) if wallet.expanded else [] for method in methods: print(method.id, method.display, method.capabilities) payment_method = next( ( method for method in methods if method.id == os.environ["LINK_PAYMENT_METHOD_ID"] ), None, ) if payment_method is None or ( payment_method.capabilities.single_use_card is not None and payment_method.capabilities.single_use_card.eligible is False ): raise RuntimeError("select an available payment method") ``` ```bash CLI theme={null} kernel vaults wallets payment-methods user-12345 link-wallet -o json ``` show the returned methods in a trusted user-facing surface, let the user choose one, and set its id as `LINK_PAYMENT_METHOD_ID`. missing capability metadata means eligibility is unknown. only `eligible: false` is an explicit negative result. ## Create and authorize a card item ```typescript TypeScript theme={null} let card = await kernel.vaults.items.upsert("notebook-order", { id_or_name: vault.id, type: "card", spec: { provider: "link", wallet: wallet.key, payment_method_id: paymentMethod.id, amount: 2306, currency: "usd", merchant_name: "example shop", merchant_url: "https://shop.example.com", context: "buy one notebook from example shop for a total of 23.06 usd, including tax " + "and shipping. this request is for this purchase only and must not be repeated.", }, }); card = await kernel.vaults.items.retrieve(card.key, { id_or_name: vault.id }); if (!card.available_operations.some(({ type }) => type === "authorize")) { throw new Error("authorization is unavailable"); } card = await kernel.vaults.items.performOperation(card.key, { id_or_name: vault.id, type: "authorize", }); if (card.action && "url" in card.action) { await presentProviderAction({ userID: authenticatedUser.id, vaultID: vault.id, item: card, }); } ``` ```python Python theme={null} card = kernel.vaults.items.upsert( "notebook-order", id_or_name=vault.id, type="card", spec={ "provider": "link", "wallet": wallet.key, "payment_method_id": payment_method.id, "amount": 2306, "currency": "usd", "merchant_name": "example shop", "merchant_url": "https://shop.example.com", "context": ( "buy one notebook from example shop for a total of 23.06 usd, including tax " "and shipping. this request is for this purchase only and must not be repeated." ), }, ) card = kernel.vaults.items.retrieve(card.key, id_or_name=vault.id) if not any(operation.type == "authorize" for operation in card.available_operations): raise RuntimeError("authorization is unavailable") card = kernel.vaults.items.perform_operation( card.key, id_or_name=vault.id, type="authorize", ) if card.action is not None and hasattr(card.action, "url"): present_provider_action( user_id=authenticated_user.id, vault_id=vault.id, item=card, ) ``` ```bash CLI theme={null} kernel vaults cards create user-12345 notebook-order \ --provider link \ --spec '{ "wallet": "link-wallet", "payment_method_id": "pm_123", "amount": 2306, "currency": "usd", "merchant_name": "example shop", "merchant_url": "https://shop.example.com", "context": "buy one notebook from example shop for a total of 23.06 usd, including tax and shipping. this request is for this purchase only and must not be repeated." }' kernel vaults items get user-12345 notebook-order -o json kernel vaults items invoke user-12345 notebook-order authorize --open kernel vaults items get user-12345 notebook-order --wait 60 -o json ``` `amount` uses minor currency units, so `2306` means 23.06 usd. link accepts values from 1 to 500000. `context` must contain at least 100 characters. card creation is live-only, and `spec.test` is not supported. `merchant_url` supplies provider context. KERNEL derives its registrable domain into `state.domains` when authorization starts, but `state.domains` is metadata rather than an enforced browser-origin allowlist. ## Use the aliases after the user completes the approval action, retrieve the card with `wait: 60` until it becomes `ready`. pass `state.aliases` to the [browser agent payments guide](/docs/browsers/enable-payments-in-browser-agent). the first recognized processor request that contains the aliases consumes the item and clears its encrypted card value. `consumed` means the credential was substituted, not that the processor accepted the payment or the merchant created an order. don't repeat `authorize` or create a replacement item to retry an unknown purchase. inspect item events and the merchant's order state first. # Stagehand Source: https://kernel.sh/docs/integrations/stagehand [Stagehand](https://github.com/browserbase/stagehand) is an open source AI browser automation framework. It lets developers choose what to write in code vs. natural language. By integrating with Kernel, you can run Stagehand automations with cloud-hosted browsers. This guide covers both Stagehand SDK v4 and v3. v4 runs as a Chrome extension alongside the browser rather than driving it purely over CDP, so a remote Kernel browser needs the extension loaded into it — the version tabs below show each setup. To move between versions, see the [Stagehand migration guide](https://docs.stagehand.dev). The CLI template uses v4. A complete, runnable computer use agent built with Stagehand and Google's Gemini 2.5 lives in the [Stagehand + Kernel cookbook](https://github.com/kernel/cookbooks/tree/main/integrations/stagehand-google-cua-agent). ## Quick start with the Stagehand template The fastest way to run Stagehand on Kernel is our app template, which comes pre-wired for v4: ```bash theme={null} kernel create --name my-stagehand-app --language typescript --template stagehand ``` This scaffolds a self-contained app with two files: * `index.ts` — the automation (searches a startup on Y Combinator and extracts its team size). * `stagehand-extension.ts` — a helper that loads the Stagehand extension onto the Kernel browser. Set a provider-prefixed `MODEL` and its API key in a `.env` file: ```bash .env theme={null} # MODEL is provider-prefixed, e.g. anthropic/claude-sonnet-4-5, openai/gpt-4.1, google/gemini-2.5-flash MODEL=anthropic/claude-sonnet-4-5 MODEL_API_KEY=your-api-key ``` Then deploy and invoke: ```bash theme={null} kernel deploy index.ts --env-file .env kernel invoke ts-stagehand teamsize-task --payload '{"company": "kernel"}' # → {"teamSize":"6"} ``` See the [deploy](/docs/apps/deploy) and [invoke](/docs/apps/invoke) guides for more. ## Adding Kernel to an existing Stagehand project If you already have a Stagehand implementation, switch it to Kernel's cloud browsers by updating your browser setup. The steps differ between v4 and v3 — pick your version below. **1. Install the Kernel SDK** ```bash theme={null} npm install @onkernel/sdk ``` **2. Load the Stagehand extension onto the Kernel browser** Stagehand v4 runs as a Chrome extension. When `localBrowser.connect` is called without an `extensionId`, Stagehand loads the extension into the running browser over CDP (`Extensions.loadUnpacked`), reading it from a path on the **browser's** filesystem. Mirror the extension — shipped inside the `@browserbasehq/stagehand` package — onto the running Kernel browser at that exact path first: ```typescript theme={null} import { Kernel } from "@onkernel/sdk"; import { createReadStream } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; const stagehandDist = dirname(fileURLToPath(import.meta.resolve("@browserbasehq/stagehand"))); const STAGEHAND_EXTENSION_ZIP = join(stagehandDist, "assets/stagehand-extension.zip"); const STAGEHAND_EXTENSION_DIR = join(stagehandDist, "extension"); async function loadStagehandExtension(kernel: Kernel, sessionId: string): Promise { await kernel.browsers.fs.uploadZip(sessionId, { dest_path: STAGEHAND_EXTENSION_DIR, zip_file: createReadStream(STAGEHAND_EXTENSION_ZIP), }); } ``` **3. Create a browser and connect** Create a Kernel browser, load the extension, then connect Stagehand to its CDP URL: ```typescript theme={null} import { Stagehand, localBrowser } from "@browserbasehq/stagehand"; import Kernel from "@onkernel/sdk"; const kernel = new Kernel(); const kernelBrowser = await kernel.browsers.create({ stealth: true }); console.log("Live view url:", kernelBrowser.browser_live_view_url); await loadStagehandExtension(kernel, kernelBrowser.session_id); // With no `extensionId`, Stagehand loads the extension over CDP. const browser = await localBrowser.connect({ cdpUrl: kernelBrowser.cdp_ws_url }); const stagehand = await Stagehand.create({ browser, model: { modelName: "anthropic/claude-sonnet-4-5", apiKey: process.env.MODEL_API_KEY, }, }); ``` **4. Use your Stagehand automation** Drive the page with Stagehand's primitives. Note the v4 API: page access is async (`activePage()`), and `extract` returns its result under `data`: ```typescript theme={null} import { z } from "zod"; const page = await browser.context.activePage(); if (!page) throw new Error("No active page in the Kernel browser"); await page.goto("https://www.ycombinator.com/companies"); await stagehand.act("Type in kernel into the search box"); await stagehand.act("Click on the first search result"); const { data } = await stagehand.extract( "Extract the team size (number of employees) shown on this Y Combinator company page.", z.object({ teamSize: z.string() }), ); console.log("Team size:", data.teamSize); ``` **5. Clean up** Stagehand v4 only closes browsers it launched, so close the connection and delete the Kernel browser yourself. Nest the cleanup so a failed `close()` never skips deleting the browser: ```typescript theme={null} try { await stagehand.close(); } finally { try { await browser.close(); } finally { await kernel.browsers.deleteByID(kernelBrowser.session_id); } } ``` **1. Install the Kernel SDK** ```bash theme={null} npm install @onkernel/sdk ``` **2. Initialize Kernel and create a browser** Import the libraries and create a cloud browser session: ```typescript theme={null} import { Stagehand } from "@browserbasehq/stagehand"; import Kernel from "@onkernel/sdk"; import { z } from "zod"; const kernel = new Kernel(); const kernelBrowser = await kernel.browsers.create({ stealth: true }); console.log("Live view url: ", kernelBrowser.browser_live_view_url); ``` **3. Update your browser configuration** Replace your existing browser setup to use Kernel's CDP URL: ```typescript theme={null} const stagehand = new Stagehand({ env: "LOCAL", localBrowserLaunchOptions: { cdpUrl: kernelBrowser.cdp_ws_url, }, model: "openai/gpt-4.1", apiKey: process.env.OPENAI_API_KEY, verbose: 1, domSettleTimeout: 30_000 }); await stagehand.init(); ``` **4. Use your Stagehand automation** Use Stagehand's page methods with the Kernel-powered browser: ```typescript theme={null} const page = stagehand.context.pages()[0]; await page.goto("https://onkernel.com"); await stagehand.act("Click on Blog in the navbar"); await stagehand.act("Click on the newest blog post"); const output = await stagehand.extract( "Extract a summary of the blog post", z.object({ summary: z.string() }) ); console.log("Newest blog post summary: ", output.summary); // Clean up await stagehand.close(); await kernel.browsers.deleteByID(kernelBrowser.session_id); ``` ## Benefits of using Kernel with Stagehand * **No local browser management**: Run automations without installing or maintaining browsers locally * **Scalability**: Launch multiple browser sessions in parallel * **Stealth mode**: Built-in anti-detection features for web scraping * **Session state**: Maintain browser state across runs via [Profiles](/docs/auth/profiles) * **Live view**: Debug your automations with real-time browser viewing ## Next steps * Check out [live view](/docs/browsers/live-view) for debugging your automations * Learn about [stealth mode](/docs/browsers/bot-detection/stealth) for avoiding detection * Learn how to properly [terminate browser sessions](/docs/browsers/termination) * Learn how to [deploy](/docs/apps/deploy) your Stagehand app to Kernel # Overview Source: https://kernel.sh/docs/integrations/stripe-projects Provision Kernel cloud browsers and plans through the Stripe Projects CLI Kernel is a Stripe Projects provider. Use the Stripe CLI to link your Kernel account, choose a plan, and provision per-project API keys without using the dashboard for setup. This page is the provider-level LLM context for agents (`stripe projects llm-context`). Service-specific guidance for cloud browser API keys is in [Stripe Projects — browser API access](/docs/integrations/stripe-projects-browser). ## Service catalog Stripe service slugs are prefixed with `kernel/` (from the app manifest name). | Service ID | Kind | Scope | Summary | | --------------------------- | ---------- | ------- | -------------------------------------------------------------- | | `kernel/plan:developer` | plan | account | Free tier — \$5/mo credits, 5 concurrent browsers | | `kernel/plan:hobbyist` | plan | account | \$30/mo, \$10/mo credits, 10 concurrent browsers (email KYC) | | `kernel/plan:startup` | plan | account | \$200/mo, \$50/mo credits, 150 concurrent browsers (email KYC) | | `kernel/browser:api-access` | deployable | project | API key for launching browsers in a Stripe Project | * Only **one plan** can be active per Kernel org at a time (`allowed_updates` defines upgrade/downgrade paths). * `browser:api-access` is **free** when any active plan is provisioned; otherwise it is usage-based by browser type (\$0.001/min headless, \$0.008/min headful, \$0.048/min headful + GPU; stealth included). ## Common CLI flows ```bash theme={null} # Link Kernel to your Stripe Projects workspace stripe projects link kernel # Browse the catalog (confirm exact slugs before provisioning) stripe projects services list # Provision a paid plan (requires verified email / KYC) stripe projects add kernel/plan:hobbyist stripe projects add kernel/plan:startup # Provision a project-scoped API key (configuration: {"name": "production"}) stripe projects add kernel/browser:api-access --config '{"name":"production"}' # Sync credentials into .env stripe projects env --pull # Rotate API key credentials stripe projects rotate kernel/browser:api-access # Open the Kernel dashboard stripe projects open kernel --purpose dashboard ``` Run `stripe projects services list` and copy the **exact** service slug from the output before `stripe projects add` — do not guess slugs. ## Account and project mapping | Stripe | Kernel | | -------------------- | -------------------------------------------------------- | | Stripe account | Kernel organization (billing + linking) | | Stripe Project | Kernel project (API keys, sessions, browser pools, etc.) | | Plan resources | Org-wide subscription tier | | `browser:api-access` | Project-scoped API key | Each Stripe Project maps 1:1 to a Kernel project within the org. Resources provisioned for a new `project_id` create a Kernel project if needed. ## Authentication New accounts use the **agentic credentials** flow: after `stripe projects link` / account request, Kernel returns bearer access and refresh tokens. Use `stripe projects env --pull` to load credentials locally. * **Dashboard (humans):** [https://dashboard.onkernel.com](https://dashboard.onkernel.com) * **Existing Kernel customers** with billing already on file may receive `payment_credentials: "provider"` when linking to avoid double-billing. ## Billing and Shared Payment Tokens (SPT) Paid plans are charged through Stripe **Shared Payment Tokens** on the Metronome-managed Stripe customer. The default SPT monthly spending limit is often **\$50**. The Start-Up plan is **\$200/mo**. Before upgrading to Start-Up, confirm the developer’s SPT limit covers the plan (`stripe projects billing show`) or ask them to raise the limit. Charges above the limit will fail. ## Full documentation * Doc index for agents: [https://kernel.sh/docs/llms.txt](https://kernel.sh/docs/llms.txt) * Create and connect browsers: [https://kernel.sh/docs/introduction/create](https://kernel.sh/docs/introduction/create) * API keys: [https://kernel.sh/docs/info/api-keys](https://kernel.sh/docs/info/api-keys) * Pricing: [https://kernel.sh/docs/info/pricing](https://kernel.sh/docs/info/pricing) * Cursor / Claude skills: [https://github.com/kernel/skills](https://github.com/kernel/skills) # Stripe Projects — browser API access Source: https://kernel.sh/docs/integrations/stripe-projects-browser LLM context for the kernel/browser:api-access Stripe Projects service This page describes the **`kernel/browser:api-access`** deployable service provisioned through Stripe Projects. ## What this resource is A **project-scoped Kernel API key** used to create and manage cloud browsers (CDP, Playwright, Computer Controls, etc.) for that Stripe Project’s Kernel project. ## Provisioning ```bash theme={null} stripe projects add kernel/browser:api-access --config '{"name":"production"}' stripe projects env --pull ``` ### Configuration | Field | Type | Required | Description | | ------ | ------ | -------- | ---------------------------------------------------- | | `name` | string | yes | Label for the API key (e.g. `production`, `staging`) | ## After provisioning 1. Run `stripe projects env --pull` to sync credentials into `.env` / `.projects/vault`. 2. Use the Kernel API key from the environment (typically `KERNEL_API_KEY`) with the SDK or CLI. ```typescript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); // uses KERNEL_API_KEY from env const browser = await kernel.browsers.create(); ``` ```bash theme={null} kernel browsers create ``` ## Pricing | Situation | Price | | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | No active Kernel plan on the org | Usage-based by browser type: **$0.001/min** headless, **$0.008/min** headful, **\$0.048/min** headful + GPU (stealth included) | | Active `plan:developer`, `plan:hobbyist`, or `plan:startup` | **Included** with plan (component pricing `free` under parent plan) | Provision a plan first when the developer needs predictable monthly credits and concurrency limits. ## Operations ```bash theme={null} # Rotate credentials (invalidates old key) stripe projects rotate kernel/browser:api-access # Remove the resource stripe projects remove kernel/browser:api-access ``` ## Further reading * [Stripe Projects (provider overview)](/docs/integrations/stripe-projects) * [Create a browser](https://kernel.sh/docs/introduction/create) * [API keys](https://kernel.sh/docs/info/api-keys) * [Projects](https://kernel.sh/docs/info/projects) # Terraform Source: https://kernel.sh/docs/integrations/terraform Manage durable Kernel infrastructure with Terraform Use the official Kernel Terraform provider to manage durable infrastructure such as projects and browser pools. ## Install the provider Add `kernel/kernel` to your Terraform configuration: ```hcl theme={null} terraform { required_providers { kernel = { source = "kernel/kernel" } } } provider "kernel" {} ``` Set `KERNEL_API_KEY` and, when needed, `KERNEL_PROJECT_ID` in the environment where Terraform runs. You can then manage Kernel projects and browser pools, read existing projects, profiles, proxies, and extensions, and import existing projects or browser pools by ID. The provider manages durable desired configuration. Use the Kernel SDKs or API for runtime browser operations such as creating sessions, acquiring browsers, releasing browsers, and viewing logs. ## Provider documentation Browse installation instructions and the generated resource and data source reference. View examples, release notes, source code, and contribution guidance. # Val Town Source: https://kernel.sh/docs/integrations/valtown [Val Town](https://www.val.town/) is a serverless platform to run Typescript automations. You can think of it like Zapier for software developers. Because Val Town can't launch a local browser, Kernel lets you use tools like Puppeteer, Playwright, or Stagehand on Val Town by connecting to a remote browser session hosted on Kernel's infrastructure. ## Quick start with Puppeteer 1. Create a free [Kernel account](https://dashboard.onkernel.com/sign-up) and generate an API key from Settings → API Keys 2. Remix the [Kernel Puppeteer val](https://www.val.town/x/kernel/puppeteer) 3. Add your `KERNEL_API_KEY` to the val's Environment Variables (left sidebar) 4. Click **Run** 5. View logs for output ## Quick start with Playwright 1. Remix the [Kernel Playwright val](https://www.val.town/x/kernel/playwright) 2. Add your `KERNEL_API_KEY` to Environment Variables 3. Set `TMPDIR` to `/tmp` to avoid Playwright file-system warnings 4. Click **Run** 5. Check the logs for output ## Quick start with Stagehand + Gemini Run an autonomous browser agent powered by Gemini 2.5's computer use capabilities and [Stagehand](https://www.stagehand.dev/). 1. Remix the [Kernel Stagehand + Gemini val](https://www.val.town/x/kernel/stagehand-gemini) 2. Add the following to Environment Variables: * `KERNEL_API_KEY` — from [Kernel's API keys page](https://dashboard.onkernel.com/settings/api-keys) * `GOOGLE_API_KEY` — from [Google AI Studio](https://aistudio.google.com/apikey) 3. Import `runAgent` into your val and call it with a task, starting URL, and timeout: ```ts theme={null} const result = await runAgent( "Find the top Hacker News story and summarize it", "https://news.ycombinator.com", 120, ); ``` 4. The agent returns an object with a `success` status and a `message` containing the results. # Agent Browser Source: https://kernel.sh/docs/integrations/vercel/agent-browser [Agent Browser](https://github.com/vercel-labs/agent-browser) is a headless browser automation CLI for AI agents built by Vercel. It provides a fast Rust CLI with Node.js fallback, making it ideal for AI-powered browser automation. By integrating with Kernel, you can run Agent Browser automations with cloud-hosted browsers. ## Using the native Kernel provider Agent Browser has built-in support for Kernel as a cloud browser provider. This is the simplest way to use Kernel with Agent Browser. ### Quick start Use the `-p` flag to enable Kernel: ```bash theme={null} export KERNEL_API_KEY="your-api-key" agent-browser -p kernel open https://example.com ``` Get your API key from the [Kernel Dashboard](https://dashboard.onkernel.com/api-keys). ### Configuration options Configure Kernel via environment variables: | Variable | Description | Default | | ------------------------ | ----------------------------------------------------------- | ------- | | `AGENT_BROWSER_PROVIDER` | Set to `kernel` as an alternative to the `-p kernel` flag | (none) | | `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 | (none) | ### 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. ```bash theme={null} export KERNEL_API_KEY="your-api-key" export KERNEL_PROFILE_NAME="my-profile" agent-browser -p kernel open https://example.com ``` ## Connecting via CDP (alternative) Use this approach when you need full control of the Kernel browser session creation logic beyond what the agent-browser environment variables support. ```bash theme={null} # Create a Kernel browser and extract the CDP URL SESSION=$(kernel browsers create --stealth -o json) CDP_URL=$(echo "$SESSION" | jq -r '.cdp_ws_url') SESSION_ID=$(echo "$SESSION" | jq -r '.session_id') # Connect agent-browser to the Kernel session agent-browser connect "$CDP_URL" # Run your automation agent-browser open https://example.com agent-browser snapshot # Clean up agent-browser close kernel browsers delete "$SESSION_ID" ``` ## Programmatic usage Use this approach if you want to use agent-browser as an alternative to Playwright within a Node.js or Python application while maintaining programmatic control over browser session lifecycle. ```typescript theme={null} import Kernel from '@onkernel/sdk'; import { execSync } from 'child_process'; const kernel = new Kernel(); const browser = await kernel.browsers.create({ stealth: true }); console.log("Live view url:", browser.browser_live_view_url); try { execSync(`agent-browser connect "${browser.cdp_ws_url}"`, { stdio: 'inherit' }); execSync('agent-browser open https://example.com', { stdio: 'inherit' }); execSync('agent-browser snapshot', { stdio: 'inherit' }); execSync('agent-browser close', { stdio: 'inherit' }); } finally { await kernel.browsers.deleteByID(browser.session_id); } ``` ```python theme={null} import subprocess from kernel import Kernel kernel = Kernel() browser = kernel.browsers.create(stealth=True) print(f"Live view url: {browser.browser_live_view_url}") try: subprocess.run(["agent-browser", "connect", browser.cdp_ws_url], check=True) subprocess.run(["agent-browser", "open", "https://example.com"], check=True) subprocess.run(["agent-browser", "snapshot"], check=True) subprocess.run(["agent-browser", "close"], check=True) finally: kernel.browsers.delete_by_id(browser.session_id) ``` ## Benefits of using Kernel with Agent Browser * **No local browser management**: Run automations without installing or maintaining browsers locally * **Scalability**: Launch multiple browser sessions in parallel * **Stealth mode**: Built-in anti-detection features for web scraping * **Session state**: Maintain browser state across runs via [Profiles](/docs/auth/profiles) * **Live view**: Debug your automations with real-time browser viewing ## Next steps * Check out [live view](/docs/browsers/live-view) for debugging your automations * Learn about [stealth mode](/docs/browsers/bot-detection/stealth) for avoiding detection * Learn how to properly [terminate browser sessions](/docs/browsers/termination) # AI SDK Tool Source: https://kernel.sh/docs/integrations/vercel/ai-sdk Build AI Agents with Kernel powered browser automation capabilities ## Overview The `@onkernel/ai-sdk` package provides Vercel AI SDK-compatible tools for browser automation powered by Kernel. This package exposes a Playwright execution tool that allows LLMs to browse the web, interact with websites, and perform automation tasks through natural language instructions. With this tool, AI agents can execute Playwright code on Kernel's remote browsers, enabling powerful browser automation capabilities in your AI-powered applications. A complete, runnable agent built on this tool lives in the [AI SDK + Kernel cookbook](https://github.com/kernel/cookbooks/tree/main/integrations/ai-sdk-agent). ## Installation Install the package along with its peer dependencies: ```bash theme={null} npm install @onkernel/ai-sdk zod npm install ai @onkernel/sdk ``` ## Prerequisites Before using the AI SDK tool, you'll need: 1. **Kernel API Key** - Obtain from the [Kernel Dashboard](https://dashboard.onkernel.com) or the [Vercel Marketplace integration](/docs/integrations/vercel/marketplace) 2. **AI Model Provider** - An API key for your chosen LLM provider (OpenAI, Anthropic, etc.) 3. **Kernel Browser Session** - A running browser session created via the Kernel SDK ## How It Works The `playwrightExecuteTool` creates a Vercel AI SDK tool that: 1. Accepts natural language instructions from an LLM 2. Converts those instructions into Playwright code 3. Executes the code on a Kernel remote browser 4. Returns the results back to the LLM This enables AI agents to autonomously browse websites, extract data, and perform complex automation tasks. ## Usage with `generateText()` The simplest way to use the AI SDK tool is with Vercel's `generateText()` function: ```typescript theme={null} import 'dotenv/config'; import { openai } from '@ai-sdk/openai'; import { playwrightExecuteTool } from '@onkernel/ai-sdk'; import { Kernel } from '@onkernel/sdk'; import { generateText } from 'ai'; // 1) Create Kernel client and start a browser session const kernel = new Kernel({ apiKey: process.env.KERNEL_API_KEY, }); const browser = await kernel.browsers.create({}); const sessionId = browser.session_id; console.log('Browser session started:', sessionId); console.log('Browser session URL:', browser.browser_live_view_url); // 2) Create the Playwright execution tool const playwrightTool = playwrightExecuteTool({ client: kernel, sessionId }); // 3) Use with Vercel AI SDK const result = await generateText({ model: openai('gpt-5.1'), prompt: 'Open news.ycombinator.com and get the title of the top news story.', tools: { playwright_execute: playwrightTool, }, }); console.log('Result:', result.toolResults[0].output); // 4) Clean up await kernel.browsers.deleteByID(sessionId); ``` ## Usage with `Agent()` For more complex, multi-step automation tasks, use the Vercel AI SDK's `Agent()` class. Agents can autonomously plan and execute a series of actions to accomplish a goal: ```typescript theme={null} import 'dotenv/config'; import { openai } from '@ai-sdk/openai'; import { playwrightExecuteTool } from '@onkernel/ai-sdk'; import { Kernel } from '@onkernel/sdk'; import { Experimental_Agent as Agent, stepCountIs } from 'ai'; const kernel = new Kernel({ apiKey: process.env.KERNEL_API_KEY, }); const browser = await kernel.browsers.create({}); const sessionId = browser.session_id; console.log('Browser session started:', sessionId); console.log('Browser session URL:', browser.browser_live_view_url); // Initialize the AI agent with GPT-5.1 const agent = new Agent({ model: openai('gpt-5.1'), tools: { playwright_execute: playwrightExecuteTool({ client: kernel, sessionId }), }, stopWhen: stepCountIs(20), // Maximum 20 steps system: `You are a browser automation expert. You help users execute tasks in their browser using Playwright.`, }); // Execute the agent with the user's task const result = await agent.generate({ prompt: 'Go to news.ycombinator.com and get the titles of the top 3 posts.', }); console.log('Agent response:', result.text); await kernel.browsers.deleteByID(browser.session_id); ``` ## Tool Parameters The `playwrightExecuteTool` function accepts the following parameters: ```typescript theme={null} function playwrightExecuteTool(options: { client: kernel; // Kernel SDK client instance sessionId: string; // Existing browser session ID }): Tool; ``` ### Tool Input Schema The generated tool accepts the following input from the LLM: ```typescript theme={null} { code: string; // Required: JavaScript/TypeScript code to execute timeout_sec?: number; // Optional: Execution timeout in seconds (default: 60) } ``` Under the hood, the tool calls: ```typescript theme={null} client.browsers.playwright.execute(sessionId, { code, timeout_sec }) ``` So, any code you can run through the SDK can be run via the tool. ## Additional Resources Official Vercel AI SDK documentation Complete Kernel SDK API reference Learn more about Kernel's Playwright execution ## Related * [Vercel Marketplace Integration](/docs/integrations/vercel/marketplace) * [Browser Creation](/docs/introduction/create) * [Stealth Mode](/docs/browsers/bot-detection/stealth) * [Live View](/docs/browsers/live-view) # Eve Extension Source: https://kernel.sh/docs/integrations/vercel/eve-extension Give your Vercel Eve agent a Kernel cloud browser in 1 loc ## Overview The [`@onkernel/eve-extension`](https://www.npmjs.com/package/@onkernel/eve-extension) package is a [Vercel Eve](https://vercel.com/eve) extension that gives your agent a Kernel cloud browser. Mount it and Kernel's browser toolset plus a `browse` skill show up under your mount automatically, so there's no browser tool code to write or maintain. See [included tools and skills](#included-tools-and-skills) for the full toolset. The tools aren't reimplemented in the extension. It packages a single MCP connection to [Kernel's hosted MCP server](https://github.com/onkernel/kernel-mcp-server), and Eve discovers the tools at runtime under your mount namespace (e.g. `kernel__browser__manage_browsers`). You can authenticate through [Vercel Connect](https://vercel.com/connect) or with a static Kernel API key. Connect is the recommended setup: no API key touches your app, and each user authenticates as themselves. Either way it's 1 loc, and you pick the auth model in the mount config. ## Prerequisites * **Node 24+** * An Eve agent project running **Eve `>= 0.25`**, which extensions require. Older Eve silently ignores `agent/extensions/` and nothing mounts. If you don't have a project yet: ```bash theme={null} npx eve@latest init my-agent && cd my-agent ``` * A [Kernel account](https://dashboard.onkernel.com), with either a Vercel Connect Kernel connector (recommended path below) or a Kernel API key * For the Connect path, **Vercel CLI `>= 58.8.0`**, which is where `--connection-method` landed. Older CLIs reject the flag. ## Setup with Vercel Connect (recommended) Vercel Connect is the recommended path because: * No key touches your app, environment, or the model. * Each user authenticates as themselves with a one-time consent that's cached afterward. * Per-user identity is a good fit for Kernel's [managed auth](/docs/auth/overview). **1. Install** the extension: ```bash theme={null} pnpm add @onkernel/eve-extension ``` **2. Create and attach the Kernel connector** in Vercel Connect. Name it `kernel-mcp` so the mount snippet works unedited: ```bash theme={null} vercel connect create kernel --name kernel-mcp --connection-method mcp vercel connect attach kernel/kernel-mcp ``` You can also add it from the Vercel dashboard under [**Connectors → Browse all**](https://vercel.com/onkernel/~/connect/browse): look up "Kernel" and choose **MCP**. Confirm the UID with `vercel connect list`. **3. Mount the extension** in one line, passing the connector UID: ```typescript theme={null} // agent/extensions/kernel.ts import kernel from "@onkernel/eve-extension"; export default kernel({ connect: "kernel/kernel-mcp" }); ``` **4. Run it:** ```bash theme={null} npx eve dev # or: npx eve deploy ``` Leave `KERNEL_API_KEY` unset. The first time a user drives the browser, Eve surfaces a Connect consent prompt; they approve once and it's cached from then on, persisting across threads and sessions. Connectors created before Kernel was in the registry keep working. Leave them as they are and point `connect` at the UID `vercel connect list` prints for them, e.g. `mcp.onkernel.com/eve-extension`. ## Included tools and skills Once mounted, the agent has the following tools, namespaced under your mount (e.g. `kernel__browser__*`; discover the exact names via `connection_search`): * **`manage_browsers`**: create, list, get, and delete browser sessions. Returns a `session_id` and a `live_view_url` you can watch or take over. * **`execute_playwright_code`**: run Playwright against the live page to read, navigate, click, or type. * **`computer_action`**: human-like mouse, keyboard, and screenshot controls for the same session. * **`manage_auth_connections`**: Kernel's [managed auth](/docs/auth/overview), so the agent logs into sites through a stored connection or a hosted login flow instead of typing credentials into the page. * **`manage_profiles`**: create and reuse browser [profiles](/docs/auth/profiles) (persistent cookies, logins, storage). * **`manage_proxies`**: create and attach [proxies](/docs/proxies/overview) (datacenter, ISP, residential, mobile) with geo-targeting. * **`manage_replays`**: start, stop, and list video replay recordings for a session, so you can capture what the agent did as an MP4. Requires a paid Kernel plan. * the **`browse` skill**: the loop the model follows to drive the browser end to end. The `browse` skill runs autonomously but is human-in-the-loop friendly: * It surfaces the live-view URL so you can take over. * It hands off for sign-ins, ambiguous choices, and sensitive actions. * It defaults to Kernel managed auth for authenticated sites. A few heavier tools are off by default to keep an autonomous agent's blast radius small on a shared API key. Add any of them via a [connection override](#overriding-the-connection): * `browser_curl`: raw HTTP through the session. * `manage_credentials`: create, read, and delete stored credentials (the managed-auth flow above works without it). * `exec_command`: shell exec in the VM. * `manage_browser_pools`: manage pools of pre-provisioned browsers. The default mount has no approval gate, and its toolset can run arbitrary JS in the browser VM (`execute_playwright_code`) and reuse logged-in sessions (`manage_auth_connections`). On a shared `KERNEL_API_KEY`, every agent user effectively acts as your whole org. * For a **personal or single-tenant** agent, the default is fine. * For **team or multi-tenant** deployments, add an approval gate via a [connection override](#overriding-the-connection): `approval: once()` (per session) or `approval: always()` (every controlled action). ## Authenticate with an API key instead One shared credential, no connector setup. A good fit for a single-tenant or personal agent. **1. Install** the extension: ```bash theme={null} pnpm add @onkernel/eve-extension ``` **2. Get a Kernel API key** at [dashboard.onkernel.com/api-keys](https://dashboard.onkernel.com/api-keys) and set it in the agent's environment: ```bash theme={null} # local dev: in the agent's .env.local KERNEL_API_KEY=sk_... # deploying to Vercel npx vercel env add KERNEL_API_KEY ``` **3. Mount the extension** in a single file that reads `KERNEL_API_KEY` from the environment: ```typescript theme={null} // agent/extensions/kernel.ts export { default } from "@onkernel/eve-extension"; ``` **4. Run:** `npx eve dev` or `npx eve deploy`. To pass the key explicitly instead of via the environment: ```typescript theme={null} // agent/extensions/kernel.ts import kernel from "@onkernel/eve-extension"; export default kernel({ apiKey: process.env.KERNEL_API_KEY }); ``` ### Configuration `kernel({ ... })` accepts: | Option | Default | Purpose | | --------- | ------------------------ | ----------------------------------------------------------------------------------------- | | `connect` | None | Vercel Connect connector UID that brokers a per-user token, so no API key is used. | | `apiKey` | `KERNEL_API_KEY` env var | Kernel API key bearer token. Used when `connect` is not set; read lazily at request time. | When `connect` is set it takes precedence. Otherwise the key is read from `apiKey`, and failing that from `KERNEL_API_KEY`. ## Overriding the connection You only need this for advanced customization: widening the tool allowlist or adding an approval gate before irreversible actions. Auth is handled by the mount config above, so you don't override for that. Mount the extension as a directory and name the connection file `browser.ts` to shadow the extension's built-in `browser` connection: ``` agent/extensions/kernel/ extension.ts # export default kernel({ connect: "kernel/kernel-mcp" }) connections/browser.ts # shadows the extension's "browser" connection ``` ```typescript theme={null} // agent/extensions/kernel/connections/browser.ts import { defineMcpClientConnection } from "eve/connections"; import { connect } from "@vercel/connect/eve"; import { always } from "eve/tools/approval"; export default defineMcpClientConnection({ url: "https://mcp.onkernel.com/mcp", description: "Kernel cloud browser.", auth: connect("kernel/kernel-mcp"), // or { getToken: async () => ({ token: process.env.KERNEL_API_KEY! }) } tools: { allow: [ "manage_browsers", "execute_playwright_code", "computer_action", "browser_curl", // high blast radius: raw HTTP through the session "manage_auth_connections", "manage_credentials", // high blast radius: create/read/delete stored credentials "manage_profiles", "manage_proxies", "manage_replays", "manage_browser_pools", // heavier tools, off by default "exec_command", // high blast radius: shell exec in the VM ], }, approval: always(), // re-check every controlled action; once() would auto-allow the rest of the session }); ``` ## Additional resources Vercel's agent framework The hosted MCP server the extension connects to Log agents into sites without handling credentials ## Related * [Vercel Marketplace Integration](/docs/integrations/vercel/marketplace) * [AI SDK Tool](/docs/integrations/vercel/ai-sdk) * [Browser Creation](/docs/introduction/create) * [Live View](/docs/browsers/live-view) # Foreman Source: https://kernel.sh/docs/integrations/vercel/foreman Give your Foreman agent a Kernel cloud browser ## Overview [Foreman](https://ask-foreman.dev) is a free, open-source software factory: specialized AI agents classify, plan, implement, and review work pulled from GitHub issues, Linear, CI failures, and more, then push draft PRs for a human to approve. Nothing ships without you — Foreman stops at the draft PR. Foreman is built on [Vercel Eve](https://vercel.com/eve), so it mounts extensions the same way any Eve agent does. ### Foreman + Kernel Mounting Kernel's [Eve extension](/docs/integrations/vercel/eve-extension) gives Foreman's implementation agent a real cloud browser, so it can go beyond reading code to reproduce bugs and verify fixes in flows that only break behind authentication: * **Sign-in, not just code review** — the agent drives an authenticated browser session to reproduce the bug the way a signed-in user would hit it. * **Per-user identity** — each teammate authenticates through their own Vercel Connect consent, so the browser acts as them rather than a shared credential. * **An approval gate** — Foreman parks its first browser action per session for the person driving it, before it acts inside a logged-in session. ### Steps 1. **Install the software factory template** — deploy [`vercel-labs/eve-software-factory-template`](https://github.com/vercel-labs/eve-software-factory-template) so Foreman is running against your repo. 2. **Add the Kernel extension** — run `eve add extension/kernel` to mount `@onkernel/eve-extension`. 3. **Add a sign-in approval gate** — wire the browser connection through Vercel Connect so it authenticates as you, and require your approval before its first action in a signed-in session, via Eve's `once()` approval (`eve/tools/approval`). That's what lets the implementation agent reproduce a bug behind auth instead of just reading the code. Do not mount the Kernel extension under `agent/subagents/`. Stations run in task mode and cannot park for a consent prompt, an approval card, or a sign-in hand-off. The copy prompt below drives your coding agent through steps 2 and 3. ## Quickstart prompt Paste this into your coding agent to add the Kernel browser to your Foreman repo. It registers the `@onkernel/eve-extension` mount, wires it to Vercel Connect instead of an API key, shadows the browser connection with an approval gate, and holds the tool allowlist to the seven tools it ships with. ## Related * [Eve Extension](/docs/integrations/vercel/eve-extension) * [Vercel Marketplace Integration](/docs/integrations/vercel/marketplace) * [Managed Auth](/docs/auth/overview) # fx Source: https://kernel.sh/docs/integrations/vercel/fx Give Vercel's fx coding agent a Kernel cloud browser via MCP ## Overview [fx](https://github.com/vercel-labs/fx) is Vercel Labs' native CLI coding agent — a minimalist, terminal-first agent with a small built-in tool surface and first-class MCP support. Instead of loading every connected server's full tool list up front, fx searches for the right capability on demand and only pulls in the tool it needs, which keeps context usage low even with several MCP servers connected. Connecting Kernel's [MCP server](/docs/reference/mcp-server) gives fx a real cloud browser: it can create a session, drive it with Playwright via [`execute_playwright_code`](/docs/reference/mcp-server/tools/execute-playwright-code), and tear it down — the same toolset described in the [MCP server reference](/docs/reference/mcp-server), including managed auth, profiles, proxies, and replays. For setup, follow the [fx client guide](/docs/reference/mcp-server/clients/fx) — it covers both the interactive OAuth path and the API-key path needed for headless runs (`fx ask`, `fx acp`, CI). ## Example With Kernel connected, fx can drive a browser from a single prompt: ```bash theme={null} fx ask "Using the kernel MCP server, create a browser session, navigate to https://news.ycombinator.com, return the titles of the top 5 articles, then delete the session." ``` fx searches its connected servers for the right capability, calls `manage_browsers` to create the session, `execute_playwright_code` to scrape the titles, and `manage_browsers` again to delete it. A run looked like this (front page changes, so yours will differ): ``` 1. AnkiDroid: Google Play no longer allowing Open Collective donation link 2. Ask HN: Who is hiring? (September 2026) 3. Urban Congestion Pricing and the Response Times of Emergency Medical Services 4. 44% on ARC-AGI-1 in 67 cents 5. Io_uring Without Readahead ``` ## Related Full setup steps, including headless auth All tools the Kernel MCP server exposes Run Playwright against a session # Marketplace Integration Source: https://kernel.sh/docs/integrations/vercel/marketplace Integrate Kernel through the Vercel Marketplace ## Integrate Kernel through the Vercel Marketplace The Vercel Marketplace allows Vercel users to install and configure third-party services, like Kernel, directly from the Vercel dashboard. Kernel offers an [official integration](https://vercel.com/marketplace/kernel) that Vercel users can install to integrate Kernel cloud browsers into their Vercel projects. Kernel works out of the box with every major agent and automation framework, including **Browser Use**, **Stagehand**, **Playwright**, **Puppeteer**, or **Computer Use** via **OpenAI**, **Anthropic**, and **Gemini**. ### Installing the Kernel Integration 1. Navigate to the [Kernel integration](https://vercel.com/marketplace/kernel) in the Vercel Marketplace. 2. Click **Install** to add Kernel to your Vercel team. 3. Select your pricing plan. 4. Provide a name for your Kernel installation (this is not used for billing or anything else). 5. Click **Create**. Vercel will automatically provision a Kernel User account, Organization, and API key for you. Vercel calls the provisioned account, Organization, and API key a **Resource**. Each Vercel team has an associated Kernel Organization. Kernel will automatically provision a new User account for you, or link to an existing Kernel User account if an account is found with the same email address. An equivalent role in Kernel is assigned to the user based on their Vercel team role. Roles and Vercel team membership are automatically synced to Kernel. ### Connecting to your Vercel projects Once you've installed Kernel, you will need to connect it to one of your Vercel projects. Generally, a Kernel Organization should be connected to a single Vercel project. Connecting to a project will automatically sync your Kernel API key to your Vercel project's environment variables. The following environment variable will be automatically synced to all environments: * `KERNEL_API_KEY` Running `vercel env pull` in your project will automatically sync the development environment variables locally. ### Configuring your Kernel integration Most of your Kernel integration configuration will be done through the Kernel Dashboard. To access the Dashboard after installing the Kernel integration, navigate to your installation details page and click the **Open in Kernel** button. ### Using Kernel in your application Once your Kernel API key is synced to your Vercel project, you can use it in your application. Here's a quick example: ```typescript TypeScript theme={null} import { Kernel } from '@onkernel/sdk'; import { chromium } from 'playwright'; const kernel = new Kernel({ apiKey: process.env.KERNEL_API_KEY }); // Create a Kernel browser const kernelBrowser = await kernel.browsers.create(); // Connect over CDP with Playwright const browser = await chromium.connectOverCDP(kernelBrowser.cdp_ws_url); try { const context = browser.contexts()[0]; const page = context.pages()[0]; await page.goto('https://example.com'); const title = await page.title(); console.log('Page title:', title); } finally { await browser.close(); await kernel.browsers.deleteByID(kernelBrowser.session_id); } ``` ```python Python theme={null} from kernel import AsyncKernel from playwright.async_api import async_playwright kernel = AsyncKernel(api_key=os.environ.get("KERNEL_API_KEY")) # Create a Kernel browser kernel_browser = await kernel.browsers.create() # Connect over CDP with Playwright playwright = await async_playwright().start() browser = await playwright.chromium.connect_over_cdp(kernel_browser.cdp_ws_url) try: context = browser.contexts[0] page = context.pages[0] await page.goto("https://example.com") title = await page.title() print(f"Page title: {title}") finally: await browser.close() await kernel.browsers.delete_by_id(kernel_browser.session_id) ``` For more examples and features like profiles, stealth mode, and live view, check out the [Browsers documentation](/docs/introduction/create). Check out our [integration guides](/docs/integrations/overview) to learn how to use Vercel + Kernel with your preferred browser automation framework. ### Pricing The pricing for plans purchased through the Vercel Marketplace is the same as the pricing when purchased directly through Kernel. For more information on Kernel's pricing and available plans, see the [Pricing page](https://onkernel.com/#pricing). Billing and usage data are reported hourly to Vercel and can be viewed directly in the Vercel Dashboard. Billing plan management can be done through both the Vercel Dashboard and the Kernel Dashboard. ### Limitations The following limitations apply when using Kernel through the Vercel Marketplace: * Vercel-managed Organizations cannot be deleted from the Kernel Dashboard. They can only be deleted by uninstalling the Kernel integration in Vercel. * Existing Kernel Organizations cannot be moved to be managed by the Kernel Vercel Integration. # Overview Source: https://kernel.sh/docs/integrations/vercel/overview Integrate Kernel with Vercel for seamless browser automation in your web applications ## Vercel + Kernel Kernel and Vercel have partnered to provide seamless browser automation capabilities for your Vercel applications. Our integration offers several ways to add browser automation to your projects: ### Agent Browser [Agent Browser](https://github.com/vercel-labs/agent-browser) is a minimal browser automation CLI for AI agents. Connect it to Kernel browsers to use our bot anti-detection, agent authentication, profile reuse, and gpu browser features. [Learn more about Agent Browser →](/docs/integrations/vercel/agent-browser) ### fx [fx](https://github.com/vercel-labs/fx) is Vercel Labs' native CLI coding agent. Connect it to Kernel's MCP server and it can create a cloud browser session, drive it with Playwright, and tear it down as part of its normal tool use. [Learn more about fx →](/docs/integrations/vercel/fx) ### AI SDK Tool for Browser Automation The `@onkernel/ai-sdk` package provides a Vercel AI SDK-compatible tool that enables AI agents to execute Playwright code on Kernel remote browsers. This tool integrates seamlessly with: * **Vercel AI SDK's `generateText()`** - For direct LLM-powered browser automation * **Vercel AI SDK's `Agent()` class** - For building autonomous browser automation agents With this tool, you can build AI-powered applications that browse the web, extract data, interact with websites, and perform complex automation tasks—all through natural language instructions. [Learn more about the AI SDK tool →](/docs/integrations/vercel/ai-sdk) ### Eve Extension The `@onkernel/eve-extension` package is a [Vercel Eve](https://vercel.com/eve) extension that gives your agent a Kernel cloud browser in 1 loc. Once mounted, Kernel's browser toolset plus a `browse` skill show up under your agent automatically: * Session management, Playwright execution, and human-like computer controls * Managed auth, browser profiles, and proxies * The `browse` skill, the loop the model follows to drive the browser end to end Authenticate per-user through Vercel Connect or with a shared Kernel API key. [Learn more about the Eve extension →](/docs/integrations/vercel/eve-extension) ### Vercel Marketplace Integration The [Vercel Marketplace integration](/docs/integrations/vercel/marketplace) allows you to install and configure Kernel directly from the Vercel dashboard. This integration: * Automatically provisions a Kernel Organization and API key * Syncs your `KERNEL_API_KEY` to your Vercel project's environment variables * Manages billing directly through Vercel * Syncs team members and roles between Vercel and Kernel [Learn more about the Marketplace integration →](/docs/integrations/vercel/marketplace) ## Use Cases * **Automated Testing** - Build AI-powered test automation * **Web Monitoring** - Monitor websites for changes or specific content * **Data Extraction** - Extract data from websites using natural language * **Form Automation** - Automate form submissions and data entry ## Next Steps Build Agents with browser automation tools Mount a Kernel browser in your Eve agent Integrate Kernel into your Vercel projects # Vibium Source: https://kernel.sh/docs/integrations/vibium [Vibium](https://github.com/VibiumDev/vibium) is a brand-new browser automation framework for AI agents that is built on WebDriver BiDi, a W3C standard. As of [v26.3.9](https://github.com/VibiumDev/vibium/releases/tag/v26.3.9), any agent can now use Vibium to connect to Kernel cloud browsers to navigate pages, fill forms, click buttons, and take screenshots. We're really excited to keep working with Jason Huggins and to keep supporting open web standards. A complete, runnable version of this integration lives in the [Vibium + Kernel cookbook](https://github.com/kernel/cookbooks/tree/main/integrations/vibium). ## Adding Kernel to existing Vibium implementations If you already have a Vibium implementation, you can switch to using Kernel's cloud browsers by creating a Kernel browser session and connecting Vibium to the returned `webdriver_ws_url`. ### 1. Install Kernel and Vibium ```bash CLI theme={null} npm install -g @onkernel/cli vibium ``` ```bash TypeScript theme={null} npm install @onkernel/sdk vibium ``` ```bash Python theme={null} pip install kernel vibium ``` ### 2. Create a Kernel browser ```bash CLI theme={null} browser_json="$(kernel browsers create -o json)" session_id="$(printf '%s' "$browser_json" | jq -r '.session_id')" webdriver_ws_url="$(printf '%s' "$browser_json" | jq -r '.webdriver_ws_url')" ``` ```typescript TypeScript theme={null} import { Kernel } from "@onkernel/sdk"; import { browser } from "vibium"; const kernel = new Kernel(); const kernelBrowser = await kernel.browsers.create(); console.log("Live view url: ", kernelBrowser.browser_live_view_url); console.log("WebDriver URL: ", kernelBrowser.webdriver_ws_url); ``` ```python Python theme={null} from kernel import Kernel from vibium.sync_api import browser kernel = Kernel() kernel_browser = kernel.browsers.create() print("Live view url: ", kernel_browser.browser_live_view_url) print("WebDriver URL: ", kernel_browser.webdriver_ws_url) ``` ### 3. Connect Vibium to your Kernel browser ```bash CLI theme={null} vibium start "$webdriver_ws_url" ``` ```typescript TypeScript theme={null} const bro = await browser.start(kernelBrowser.webdriver_ws_url); const page = await bro.page(); ``` ```python Python theme={null} bro = browser.start(kernel_browser.webdriver_ws_url) page = bro.page() ``` ### 4. Use your Vibium automation Use Vibium's page methods with the Kernel-powered browser: ```bash CLI theme={null} vibium go https://example.com vibium title # "Example Domain" vibium text h1 # "Example Domain" ``` ```typescript TypeScript theme={null} await page.go("https://example.com"); console.log(await page.title()); console.log(await page.find("h1").text()); ``` ```python Python theme={null} page.go("https://example.com") print(page.title()) print(page.find("h1").text()) ``` ### 5. Clean up When you're done, close Vibium and delete the Kernel browser session: ```bash CLI theme={null} vibium stop kernel browsers delete "$session_id" ``` ```typescript TypeScript theme={null} await bro.stop(); await kernel.browsers.deleteByID(kernelBrowser.session_id); ``` ```python Python theme={null} bro.stop() kernel.browsers.delete_by_id(kernel_browser.session_id) ``` ## Using Vibium CLI with Kernel The CLI can connect directly to the Kernel browser's WebDriver BiDi endpoint: ```bash theme={null} export KERNEL_API_KEY= browser_json="$(kernel browsers create -o json)" session_id="$(printf '%s' "$browser_json" | jq -r '.session_id')" webdriver_ws_url="$(printf '%s' "$browser_json" | jq -r '.webdriver_ws_url')" vibium start "$webdriver_ws_url" vibium go https://example.com vibium title # "Example Domain" vibium text h1 # "Example Domain" vibium stop kernel browsers delete "$session_id" ``` ## Using Vibium MCP server with Kernel The MCP server reads the same environment variable, so AI agents can use a Kernel-powered browser: ```bash theme={null} VIBIUM_CONNECT_URL= vibium mcp ``` Or in your Claude Desktop / Claude Code config: ```json theme={null} { "mcpServers": { "vibium": { "command": "vibium", "args": ["mcp"], "env": { "VIBIUM_CONNECT_URL": "" } } } } ``` ## Benefits of using Kernel with Vibium * **No local browser management**: Run automations without installing or maintaining browsers locally * **Scalability**: Launch multiple browser sessions in parallel * **Stealth mode**: Built-in anti-detection features for web scraping * **Session state**: Maintain browser state across runs via [Profiles](/docs/auth/profiles) * **Live view**: Debug your automations with real-time browser viewing ## Next steps * Check out [live view](/docs/browsers/live-view) for debugging your automations * Learn about [stealth mode](/docs/browsers/bot-detection/stealth) for avoiding detection * Learn how to properly [terminate browser sessions](/docs/browsers/termination) * Learn how to [deploy](/docs/apps/deploy) your Vibium app to Kernel # Control Source: https://kernel.sh/docs/introduction/control Drive the browser with computer use, playwright execution, CDP, or WebDriver BiDi Kernel browsers expose four ways to drive a session. For agents, we recommend starting with playwright execution and falling back to computer use, here's our guide: [playwright w/ computer use fallback](/docs/browsers/playwright-computer-use-fallback). Both run co-located with the browser and avoid the bot-detection surface a direct CDP connection introduces. Kernel's [Computer Controls](/docs/browsers/computer-controls) API exposes OS-level mouse, keyboard, and screen primitives — the surface a computer-use model already knows how to drive (screenshot, click, type, key, scroll, drag). No CDP or WebDriver connection required, so there's no protocol fingerprint to leak. Ideal for [Claude](/docs/integrations/computer-use/anthropic), [OpenAI](/docs/integrations/computer-use/openai), or [Gemini](/docs/integrations/computer-use/gemini) computer-use loops. ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); const kernelBrowser = await kernel.browsers.create(); const screenshot = await kernel.browsers.computer.captureScreenshot(kernelBrowser.session_id); await kernel.browsers.computer.clickMouse(kernelBrowser.session_id, { x: 420, y: 280, }); await kernel.browsers.computer.typeText(kernelBrowser.session_id, { text: 'kernel cloud browsers', }); ``` ```python Python theme={null} from kernel import Kernel kernel = Kernel() kernel_browser = kernel.browsers.create() screenshot = kernel.browsers.computer.capture_screenshot(id=kernel_browser.session_id) kernel.browsers.computer.click_mouse( id=kernel_browser.session_id, x=420, y=280, ) kernel.browsers.computer.type_text( id=kernel_browser.session_id, text="kernel cloud browsers", ) ``` ```go Go theme={null} package main import ( "context" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() kernelBrowser, err := client.Browsers.New(ctx, kernel.BrowserNewParams{}) if err != nil { panic(err) } screenshot, err := client.Browsers.Computer.CaptureScreenshot( ctx, kernelBrowser.SessionID, kernel.BrowserComputerCaptureScreenshotParams{}, ) if err != nil { panic(err) } defer screenshot.Body.Close() if err := client.Browsers.Computer.ClickMouse( ctx, kernelBrowser.SessionID, kernel.BrowserComputerClickMouseParams{ X: 420, Y: 280, }, ); err != nil { panic(err) } if err := client.Browsers.Computer.TypeText( ctx, kernelBrowser.SessionID, kernel.BrowserComputerTypeTextParams{ Text: "kernel cloud browsers", }, ); err != nil { panic(err) } } ``` Run any Playwright code from anywhere — no local Playwright install, no Chromium download, no CDP connection to manage. Your code executes inside the browser's VM with the full Playwright API in scope and returns structured data back to your agent. Ships with [Patchright](/docs/browsers/bot-detection/stealth) by default. ```typescript Typescript/Javascript theme={null} const response = await kernel.browsers.playwright.execute( kernelBrowser.session_id, { code: ` await page.goto('https://example.com'); return await page.title(); `, }, ); console.log(response.result); ``` ```python Python theme={null} response = kernel.browsers.playwright.execute( id=kernel_browser.session_id, code=""" await page.goto('https://example.com') return await page.title() """, ) print(response.result) ``` ```go Go theme={null} response, err := client.Browsers.Playwright.Execute( ctx, kernelBrowser.SessionID, kernel.BrowserPlaywrightExecuteParams{ Code: ` await page.goto('https://example.com'); return await page.title(); `, }, ) if err != nil { panic(err) } fmt.Println(response.Result) ``` Chrome DevTools Protocol — the wire format Playwright, Puppeteer, and most browser frameworks speak. Use `cdp_ws_url` from the created browser session for deterministic, scripted automation driven from your own infra. ```typescript Typescript/Javascript theme={null} import { chromium } from 'playwright'; const browser = await chromium.connectOverCDP(kernelBrowser.cdp_ws_url); const context = browser.contexts()[0]; const page = context.pages()[0]; await page.goto('https://example.com'); const title = await page.title(); console.log(title); ``` ```python Python theme={null} from playwright.async_api import async_playwright async with async_playwright() as playwright: browser = await playwright.chromium.connect_over_cdp(kernel_browser.cdp_ws_url) context = browser.contexts[0] page = context.pages[0] await page.goto('https://example.com') title = await page.title() print(title) ``` W3C-standard browser control. Use `webdriver_ws_url` with [Vibium](/docs/integrations/vibium) or any other BiDi client. ```typescript Typescript/Javascript theme={null} import { browser } from 'vibium'; const bro = await browser.start(kernelBrowser.webdriver_ws_url); const page = await bro.page(); await page.goto('https://example.com'); const title = await page.title(); console.log(title); ``` ```python Python theme={null} from vibium.sync_api import browser bro = browser.start(kernel_browser.webdriver_ws_url) page = bro.page() page.goto('https://example.com') title = page.title() print(title) ``` ## Why computer use for agents Kernel's computer controls are built to match how computer-use models were trained — the same primitives the model emits (screenshot, click at coords, type, key, scroll, drag) map 1:1 onto the API. There's no harness translating model output into framework calls. * **Native fit.** Screenshot, click, type, key, scroll, drag — the primitives the model already speaks. * **Faster screenshots.** Captures bypass CDP, which removes the largest source of latency in a vision loop. * **Better against bot detection.** No CDP connection means no CDP fingerprint to leak. Pairs naturally with [stealth mode](/docs/browsers/bot-detection/stealth) and [residential proxies](/docs/proxies/residential). * **Human-like input.** OS-level events with Bézier-curve mouse paths, variable typing speed, and configurable mistype rate. * **Not DOM-limited.** Screenshots capture the full VM, so the agent can see and interact with native dialogs, canvas elements, iframes, and PDFs — not just things you can address with a selector. ## Why playwright execution over a direct CDP connection If you're reaching for Playwright, prefer the execution API over `connectOverCDP`. Same Playwright API you already know, none of the setup. * **Run from anywhere.** No `playwright` package to version-pin, no Chromium download, no CDP connection to manage. Send the code, get the result. * **Co-located with the browser.** Code runs in the same VM as the browser — no network hop between your script and the page, fewer flakes. * **Patchright by default.** Hardened against bot detection out of the box. * **Full Playwright API.** `page`, `context`, and `browser` are all in scope. Anything Playwright can do — DOM queries, file uploads, full-page screenshots — works here. * **Returns values.** `return` from your code and the result comes back in the response. Easy to use as an agent tool. ## Computer use + playwright execution Computer controls drive the browser the way a person would — they don't speak the programmatic API surface. Anything you'd reach for the DOM or Playwright client for (reading text and attributes, `page.goto`, file uploads, cookie or storage access, switching tabs) belongs on the [playwright execution](/docs/browsers/playwright-execution) side. The recommended pattern for agents is computer controls for interaction, playwright execution as a tool the agent can call when it needs structured data or a programmatic action. ```typescript Typescript/Javascript theme={null} const response = await kernel.browsers.playwright.execute( kernelBrowser.session_id, { code: ` const rows = await page.$$eval('table tr', (trs) => trs.map((tr) => Array.from(tr.querySelectorAll('td')).map((td) => td.textContent)) ); return rows; `, }, ); console.log(response.result); ``` ```python Python theme={null} response = kernel.browsers.playwright.execute( id=kernel_browser.session_id, code=""" const rows = await page.$$eval('table tr', (trs) => trs.map((tr) => Array.from(tr.querySelectorAll('td')).map((td) => td.textContent)) ); return rows; """, ) print(response.result) ``` ```go Go theme={null} response, err := client.Browsers.Playwright.Execute( ctx, kernelBrowser.SessionID, kernel.BrowserPlaywrightExecuteParams{ Code: ` const rows = await page.$$eval('table tr', (trs) => trs.map((tr) => Array.from(tr.querySelectorAll('td')).map((td) => td.textContent)) ); return rows; `, }, ) if err != nil { panic(err) } fmt.Println(response.Result) ``` ## Going deeper * [Computer Controls reference](/docs/browsers/computer-controls) — every mouse, keyboard, and screen primitive. * [Playwright Execution reference](/docs/browsers/playwright-execution) — the full execution surface, return values, and timeouts. * [Computer use integrations](/docs/integrations/computer-use/anthropic) — drop-in examples for Anthropic, Gemini, OpenAI, and more. # Create Source: https://kernel.sh/docs/introduction/create Spin up a cloud browser for your agent Kernel browsers are sandboxed Chromium instances that boot in under 30ms. Your agent creates them on demand, drives them, and tears them down — no infra to provision, no servers to run. ## Your first browser Install the Kernel SDK first: * Typescript/Javascript: `npm install @onkernel/sdk` * Python: `pip install kernel` * Go: `go get github.com/kernel/kernel-go-sdk` ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); const kernelBrowser = await kernel.browsers.create(); console.log(kernelBrowser.session_id); ``` ```python Python theme={null} from kernel import Kernel kernel = Kernel() kernel_browser = kernel.browsers.create() print(kernel_browser.session_id) ``` ```go Go theme={null} package main import ( "context" "fmt" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() kernelBrowser, err := client.Browsers.New(ctx, kernel.BrowserNewParams{}) if err != nil { panic(err) } fmt.Println(kernelBrowser.SessionID) } ``` ```bash CLI theme={null} kernel browsers create # or with a starting URL and stealth kernel browsers create --stealth --start-url https://example.com ``` The response includes everything you need to drive the browser: `session_id`, `cdp_ws_url`, `webdriver_ws_url`, and `browser_live_view_url`. ## Pick the right shape Most of what you'll tune at creation time falls into four buckets: headful (default) supports live view, replays, and better stealth — ideal for agent workflows on bot-detected sites. headless is lighter (1 gb vs 8 gb), good for simple scraping. Turn on stealth mode and route through residential, ISP, or datacenter proxies when you're hitting sites with bot detection. Required for WebGL, video, and canvas-heavy workloads. Trades off standby support. Persist cookies, storage, and authenticated sessions across runs with a [profile](/docs/auth/profiles), or learn how to hand supported login flows off to Kernel with [Managed Auth](/docs/auth/overview). ## On demand or from a pool `browsers.create()` boots a browser for you on the spot. That's the right call while you're building, and for workloads that run occasionally. Once you're running the same task repeatedly — or more than a handful at a time — create a [browser pool](/docs/browsers/pools) instead. A browser pool holds browsers that are already booted with your configuration applied, so `acquire` hands you one that's ready to drive rather than starting one from scratch. Two things get faster: configurations that restart Chromium on creation (custom viewports, extensions, kiosk mode) are already applied, and acquiring from a browser pool sidesteps the [rate limit](/docs/info/pricing#rate-limiting) on browser creation that you'll otherwise hit at volume. ```typescript Typescript/Javascript theme={null} // Create the pool once, at deploy time or on startup await kernel.browserPools.create({ name: 'checkout-pool', size: 20, stealth: true }); // Then, wherever your automation runs const kernelBrowser = await kernel.browserPools.acquire('checkout-pool', {}); ``` ```python Python theme={null} # Create the pool once, at deploy time or on startup kernel.browser_pools.create(name="checkout-pool", size=20, stealth=True) # Then, wherever your automation runs kernel_browser = kernel.browser_pools.acquire("checkout-pool") ``` ```go Go theme={null} // Create the pool once, at deploy time or on startup if _, err := client.BrowserPools.New(ctx, kernel.BrowserPoolNewParams{ Name: kernel.String("checkout-pool"), Size: 20, Stealth: kernel.Bool(true), }); err != nil { panic(err) } // Then, wherever your automation runs kernelBrowser, err := client.BrowserPools.Acquire(ctx, "checkout-pool", kernel.BrowserPoolAcquireParams{}) if err != nil { panic(err) } ``` ```bash CLI theme={null} kernel browser-pools create checkout-pool --size 20 --stealth kernel browser-pools acquire checkout-pool ``` An acquired browser returns the same fields as one you created directly, so the rest of your code is identical. Idle browsers in a browser pool aren't billed — you pay only while a browser is acquired and running — though browser pool capacity does count against your [concurrency limit](/docs/info/pricing#concurrency-limits). ## Lifecycle A browser stays alive as long as something is driving it — a CDP or WebDriver client, a [Live View](/docs/browsers/live-view) viewer, or an in-flight [computer controls](/docs/browsers/computer-controls) request. After five seconds with none of those active, it enters [standby](/docs/browsers/standby) — state is preserved, billing stops. Once in standby, after the configurable timeout (60s by default) elapses it's deleted. We recommend you delete a browser explicitly when you're done with it: ```typescript Typescript/Javascript theme={null} await kernel.browsers.deleteByID(kernelBrowser.session_id); ``` ```python Python theme={null} kernel.browsers.delete_by_id(kernel_browser.session_id) ``` ```go Go theme={null} if err := client.Browsers.DeleteByID(ctx, kernelBrowser.SessionID); err != nil { panic(err) } ``` ```bash CLI theme={null} kernel browsers delete ``` See [Termination & timeouts](/docs/browsers/termination#automatic-deletion-via-timeout) for the full set of teardown options. ## Full example Putting it together — create a browser, run Playwright code inside it, tear down: ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); const kernelBrowser = await kernel.browsers.create(); try { const response = await kernel.browsers.playwright.execute( kernelBrowser.session_id, { code: ` await page.goto('https://www.onkernel.com'); return await page.title(); `, }, ); console.log(response.result); } catch (error) { console.error(error); } finally { await kernel.browsers.deleteByID(kernelBrowser.session_id); } ``` ```python Python theme={null} from kernel import Kernel kernel = Kernel() kernel_browser = kernel.browsers.create() try: response = kernel.browsers.playwright.execute( id=kernel_browser.session_id, code=""" await page.goto('https://www.onkernel.com') return await page.title() """, ) print(response.result) except Exception as e: print(e) finally: kernel.browsers.delete_by_id(kernel_browser.session_id) ``` ```go Go theme={null} package main import ( "context" "fmt" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() kernelBrowser, err := client.Browsers.New(ctx, kernel.BrowserNewParams{}) if err != nil { panic(err) } defer func() { if err := client.Browsers.DeleteByID(ctx, kernelBrowser.SessionID); err != nil { panic(err) } }() response, err := client.Browsers.Playwright.Execute( ctx, kernelBrowser.SessionID, kernel.BrowserPlaywrightExecuteParams{ Code: ` await page.goto('https://www.onkernel.com'); return await page.title(); `, }, ) if err != nil { panic(err) } fmt.Println(response.Result) } ``` ## What's next Once you have a browser, you need to drive it. Head to [Control](/docs/introduction/control) to see the four primitives Kernel exposes — computer use, playwright execution, CDP, and WebDriver BiDi — and when to reach for each. When you're ready to run this in production, the [Browser Pools overview](/docs/browsers/pools) shows how to serve your workload from a pool of pre-provisioned browsers instead of creating one per task. # Observe Source: https://kernel.sh/docs/introduction/observe Watch your agent work, debug what went wrong Browser agents fail in ways that don't show up in logs. Kernel gives you several ways to see what's actually happening: live, after the fact, frame by frame, line by line, and event by event. ## Live view Every browser exposes a `browser_live_view_url` you can open in a browser tab or embed in an iframe. Use it to watch an agent run in real time, hand a session off to a human-in-the-loop, or surface the browser as part of your own UI. ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); const kernelBrowser = await kernel.browsers.create(); console.log(kernelBrowser.browser_live_view_url); ``` ```python Python theme={null} from kernel import Kernel kernel = Kernel() kernel_browser = kernel.browsers.create() print(kernel_browser.browser_live_view_url) ``` ```go Go theme={null} package main import ( "context" "fmt" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() kernelBrowser, err := client.Browsers.New(ctx, kernel.BrowserNewParams{}) if err != nil { panic(err) } fmt.Println(kernelBrowser.BrowserLiveViewURL) } ``` ```bash CLI theme={null} kernel browsers view ``` Add `?readOnly=true` for a non-interactive view, or enable [kiosk mode](/docs/browsers/live-view#kiosk-mode) at creation for a fullscreen, cinematic experience. Full reference: [Live View](/docs/browsers/live-view). ## Replays Replays are MP4 recordings you start and stop on demand - capture as many clips per session as you need. They're the right tool for post-hoc debugging: a failed run gives you one or more videos to scrub through, share, or attach to a bug report. Replays can also be enabled on managed auth sessions, so you can [debug failed logins](https://www.kernel.sh/docs/auth/configuration#record-sessions-for-debugging) the same way. ```typescript Typescript/Javascript theme={null} const replay = await kernel.browsers.replays.start(kernelBrowser.session_id); // ...run the agent... await kernel.browsers.replays.stop(replay.replay_id, { id: kernelBrowser.session_id, }); ``` ```python Python theme={null} replay = kernel.browsers.replays.start(kernel_browser.session_id) # ...run the agent... kernel.browsers.replays.stop( replay.replay_id, id=kernel_browser.session_id, ) ``` ```go Go theme={null} replay, err := client.Browsers.Replays.Start(ctx, kernelBrowser.SessionID, kernel.BrowserReplayStartParams{}) if err != nil { panic(err) } // ...run the agent... if err := client.Browsers.Replays.Stop( ctx, replay.ReplayID, kernel.BrowserReplayStopParams{ID: kernelBrowser.SessionID}, ); err != nil { panic(err) } ``` ```bash CLI theme={null} # Start recording kernel browsers replays start # ...run the agent... # Stop and download kernel browsers replays stop kernel browsers replays download -o replay.mp4 ``` Full reference: [Replays](/docs/browsers/replays). ## Screenshots Pull a frame at any moment with computer controls - useful for snapshotting state at decision points, attaching to traces, or feeding back into a vision model. ```typescript Typescript/Javascript theme={null} const screenshot = await kernel.browsers.computer.captureScreenshot( kernelBrowser.session_id, ); ``` ```python Python theme={null} screenshot = kernel.browsers.computer.capture_screenshot( id=kernel_browser.session_id, ) ``` ```go Go theme={null} screenshot, err := client.Browsers.Computer.CaptureScreenshot( ctx, kernelBrowser.SessionID, kernel.BrowserComputerCaptureScreenshotParams{}, ) if err != nil { panic(err) } defer screenshot.Body.Close() ``` ```bash CLI theme={null} kernel browsers computer screenshot --to screenshot.png ``` For full-page captures, use [Playwright execution](/docs/browsers/playwright-execution#screenshots) instead. ## Invocation logs If you're running an agent on Kernel's [app platform](/docs/apps/develop), every invocation produces a streaming log feed. Tail it live while the agent runs, or pull it after the fact for debugging. ```typescript Typescript/Javascript theme={null} const logs = await kernel.invocations.follow(invocationId); for await (const event of logs) { console.log(event); } ``` ```python Python theme={null} logs = kernel.invocations.follow(invocation_id) for event in logs: print(event) ``` ```go Go theme={null} stream := client.Invocations.FollowStreaming( ctx, invocationID, kernel.InvocationFollowParams{}, ) defer stream.Close() for stream.Next() { fmt.Println(stream.Current()) } if err := stream.Err(); err != nil { panic(err) } ``` Full reference: [Logs](/docs/apps/logs). ## Telemetry Telemetry is a real-time, structured stream of what happens inside a session: console output, network activity, page lifecycle, interactions, and operational signals like crashes. Unlike a video or screenshot, it's machine-readable, so it's the right tool for feeding session activity into your own observability pipeline or reacting to events programmatically. Enable it at creation, then stream the events: ```typescript Typescript/Javascript theme={null} const browser = await kernel.browsers.create({ telemetry: { enabled: true } }); const stream = await kernel.browsers.telemetry.stream(browser.session_id); for await (const { seq, event } of stream) { console.log(`#${seq} [${event.category}] ${event.type}`); } ``` ```python Python theme={null} browser = kernel.browsers.create(telemetry={"enabled": True}) with kernel.browsers.telemetry.stream(browser.session_id) as stream: for envelope in stream: print(f"#{envelope.seq} [{envelope.event.category}] {envelope.event.type}") ``` ```go Go theme={null} stream := client.Browsers.Telemetry.StreamStreaming( ctx, kernelBrowser.SessionID, kernel.BrowserTelemetryStreamParams{}, ) defer stream.Close() for stream.Next() { fmt.Println(stream.Current()) } if err := stream.Err(); err != nil { panic(err) } ``` ```bash CLI theme={null} kernel browsers telemetry stream ``` Full reference: [Telemetry](/docs/browsers/telemetry/overview). ## Picking the right tool * **Building the agent?** Keep a [live view](/docs/browsers/live-view) tab open while you iterate. * **Debugging a failure?** Capture a [replay](/docs/browsers/replays) for the run, then watch the video. * **Instrumenting the agent itself?** Drop [screenshots](/docs/browsers/computer-controls#take-screenshots) and [logs](/docs/apps/logs) into your traces at the points that matter. * **Feeding an observability pipeline?** Stream [telemetry](/docs/browsers/telemetry/overview) events and route them wherever you collect signals. * **Putting a human in the loop?** Embed the [live view](/docs/browsers/live-view#embedding-in-an-iframe) in your own UI. # Scale Source: https://kernel.sh/docs/introduction/scale Recommended practices for scaling in production ## Overview This guide covers how to run Kernel in production at scale — which architecture to build around browser creation, and when to reach for a browser pool. It assumes you're comfortable [creating](/docs/introduction/create) and [controlling](/docs/introduction/control) browsers; for the mechanics of standing up a pool and acquiring from it, see [Browser Pools](/docs/browsers/pools). ## Why a browser pool A [browser pool](/docs/browsers/pools) keeps a set of identically-configured browsers ready for immediate use. Compared to creating browsers on demand, it gives you: * **Low-latency acquisition** — the browser is already booted with your configuration applied (including settings like custom viewports, extensions, and kiosk-mode live view that otherwise [restart Chromium](/docs/browsers/performance#troubleshooting-latency) on a fresh browser), so `acquire` hands you one that's ready to drive. * **Reserved, pre-configured capacity** — a fixed set of browsers on your exact configuration, ready before traffic arrives. * **Higher creation throughput** — acquiring from a pool isn't subject to the [rate limit](/docs/info/pricing#rate-limiting) on `browsers.create()` that high-volume workloads hit. The tradeoff: a browser pool counts against your concurrency limit whether or not its browsers are currently acquired — a pool sized to 40 holds 40 of your limit. Idle pooled browsers aren't billed, but they hold the slot. ## When to use a pool vs on-demand Reach for a **browser pool** when: * you're running the same workload repeatedly, in production * acquisition latency matters — a cold start is unacceptable (for example, a synchronous, user-facing action) * traffic is steady or high-frequency enough to keep the browser pool utilized * you're hitting the `browsers.create()` rate limit at volume Stick with **on-demand `browsers.create()`** when: * volume is low, bursty, one-off, or you're still developing * each session needs a different configuration (a pool is one fixed config) * you need a GPU browser (not available in pools) Concurrency and request patterns are how you *size* a pool once you've decided to use one — not a threshold that gates whether pools are worth it. Even a small pool pays off when acquisition latency matters and demand is steady. ## Sizing Watch `available_count` and target 10–20% available under normal load, resizing before traffic peaks rather than during them. See [Sizing a browser pool](/docs/browsers/pools#sizing-a-browser-pool) for the full guidance. ## Architecture patterns ### Direct browser creation (POC) For proof-of-concept work and early production systems with modest concurrency needs, creating browsers on-demand is the simplest approach. **When to use:** * Low or unpredictable volume * Infrequent or one-off workloads * Early development and testing ```typescript theme={null} import Kernel from '@onkernel/sdk'; import { chromium } from 'playwright'; const kernel = new Kernel(); async function processTask(taskData: any) { // Create browser with extended timeout for long tasks const session = await kernel.browsers.create({ stealth: true, timeout_seconds: 3600, // Destroy browser after 1 hour of inactivity }); try { const browser = await chromium.connectOverCDP(session.cdp_ws_url); const context = browser.contexts()[0]; const page = context.pages()[0]; // Your automation logic here await page.goto(taskData.url); // ... perform work ... return { success: true, data: /* results */ }; } catch (error) { console.error('Task failed:', error); return { success: false, error: error.message }; } finally { // Always clean up await kernel.browsers.deleteByID(session.session_id); } } ``` ### Single browser pool (scaling) For production systems with consistent, high-frequency workloads, a browser pool allows you to access higher concurrency plus predictable performance. **When to use:** * Consistent, high-frequency workloads on a fixed configuration * Steady request patterns, or latency-sensitive acquisition ```typescript theme={null} import Kernel from '@onkernel/sdk'; import { chromium } from 'playwright'; const kernel = new Kernel(); const POOL_NAME = 'production-pool'; // Initialize pool once (typically in deployment/startup) async function initializePool() { await kernel.browserPools.create({ name: POOL_NAME, size: 25, // Balance cost and availability timeout_seconds: 300, // Destroy browsers after 5 minutes of inactivity stealth: true, headless: false, // headless: true for cost savings if no live view needed }); } async function processTask(taskData: any) { let session; try { // Acquire browser (returns immediately if available) session = await kernel.browserPools.acquire(POOL_NAME, { acquire_timeout_seconds: 30, // Wait up to 30s for availability }); const browser = await chromium.connectOverCDP(session.cdp_ws_url); const context = browser.contexts()[0]; const page = context.pages()[0]; // Perform work await page.goto(taskData.url); // ... automation logic ... return { success: true, data: /* results */ }; } catch (error) { console.error('Task failed:', error); return { success: false, error: error.message }; } finally { // Critical: Always release back to pool if (session) { await kernel.browserPools.release(POOL_NAME, { session_id: session.session_id, reuse: true, // Reuse for efficiency }); } } } ``` **Key considerations:** * Pool size should match your typical concurrency * Always release browsers in a `finally` block to prevent browser pool exhaustion * Set `acquire_timeout_seconds` based on your SLA requirements ### Queue-based processing (high scale) For systems exceeding browser pool capacity or with unpredictable bursts, implement a task queue to manage workloads gracefully. **When to use:** * Request volume exceeds a single browser pool's capacity * Highly variable traffic patterns * Need to prioritize certain tasks * Want to decouple request ingestion from processing ```typescript theme={null} import { Queue } from 'bullmq'; // or any queue system import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); const POOL_NAME = 'production-pool'; const POOL_SIZE = 50; // Task queue configuration const taskQueue = new Queue('browser-tasks', { connection: { /* Redis config */ } }); // Worker that processes tasks async function startWorker() { const worker = new Worker('browser-tasks', async (job) => { let session; try { // Acquire browser with reasonable timeout session = await kernel.browserPools.acquire(POOL_NAME, { acquire_timeout_seconds: 120, }); const browser = await chromium.connectOverCDP(session.cdp_ws_url); const context = browser.contexts()[0]; const page = context.pages()[0]; // Process job data await page.goto(job.data.url); const result = await page.evaluate(() => /* extract data */); return { success: true, data: result }; } catch (error) { // Handle errors with retry logic if (error.message.includes('timeout')) { throw new Error('RETRY'); // BullMQ will retry } throw error; } finally { if (session) { await kernel.browserPools.release(POOL_NAME, { session_id: session.session_id, reuse: true, }); } } }, { connection: { /* Redis config */ }, concurrency: POOL_SIZE, // Match pool size }); return worker; } // Add tasks to queue async function submitTask(taskData: any, priority?: number) { await taskQueue.add('process', taskData, { priority: priority || 5, attempts: 3, backoff: { type: 'exponential', delay: 2000, }, }); } ``` **Queue-specific considerations:** * Set worker concurrency to match or slightly exceed browser pool size * Implement proper retry logic for transient failures * Monitor queue depth to scale browser pools dynamically * Use priority queues for different SLAs # Scrapybara Source: https://kernel.sh/docs/migrations/scrapybara [Scrapybara](https://scrapybara.com/) has shut down their virtual desktop and browser service as of **October 15, 2025**. If you were using Scrapybara for browser automation, Kernel is here to help you migrate seamlessly. ## Key Concepts | Feature | Scrapybara | Kernel | | ----------------- | ----------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | **Start Browser** | `client.start_browser()` | `kernel.browsers.create()` | | **Standby Mode** | `instance.pause()` / `instance.resume()` | Automatic standby mode | | **CDP URL** | `instance.get_cdp_url().cdp_url` | Returns `cdp_ws_url` in create response | | **Live View** | `instance.get_stream_url().stream_url` | Returns `browser_live_view_url` in create response | | **Stealth Mode** | ❌ Not available | Create browser with `stealth: true` | | **Replays** | ❌ Not available | `kernel.browsers.replays.start()` and `kernel.browsers.replays.stop()` | | **Save Auth** | `instance.browser.save_auth(name="default")` | Create [Profile](/docs/auth/profiles). Then create browser with `kernel.browsers.create(profile={"name": "profile1", "save_changes": true})` | | **Click** | `instance.computer(action="click_mouse", button="left")` | `kernel.browsers.computer.click_mouse(id=session_id, x=100, y=200)` | | **Drag** | `instance.computer(action="drag_mouse", path=[[100, 200], [300, 400]])` | `kernel.browsers.computer.drag_mouse(id=session_id, path=[[100, 200], [150, 220], [200, 260]])` | | **Screenshot** | `instance.computer(action="take_screenshot").base64_image` | `kernel.browsers.computer.capture_screenshot(id=session_id)` | ## How to migrate ### Basic Browser Creation **Scrapybara** ```python theme={null} from scrapybara import Scrapybara from playwright.async_api import async_playwright client = Scrapybara(api_key="your_api_key") instance = client.start_browser(timeout_hours=1) cdp_url = instance.get_cdp_url().cdp_url async with async_playwright() as p: browser = await p.chromium.connect_over_cdp(cdp_url) page = browser.contexts[0].pages[0] await page.goto("https://example.com") await browser.close() instance.stop() ``` **Kernel** ```python theme={null} from kernel import Kernel from playwright.async_api import async_playwright kernel = Kernel() kernel_browser = kernel.browsers.create(timeout_seconds=3600) cdp_url = kernel_browser.cdp_ws_url async with async_playwright() as p: browser = await p.chromium.connect_over_cdp(cdp_url) page = browser.contexts[0].pages[0] await page.goto("https://example.com") await browser.close() await kernel.browsers.delete_by_id(kernel_browser.session_id) ``` ### Save & Reuse Authentication **Scrapybara** ```python theme={null} # First session - save auth instance = client.start_browser() # ... login to website via Playwright ... auth_state = instance.browser.save_auth(name="my-login") instance.stop() # Second session - load auth instance2 = client.start_browser() instance2.browser.authenticate(auth_state_id=auth_state.auth_state_id) # ... browser now has saved cookies ... ``` **Kernel** ```python theme={null} # First session - save auth profile = await kernel.profiles.create(name="my-login") kernel_browser = await kernel.browsers.create( profile={"name": "my-login", "save_changes": True} ) # ... login to website via Playwright ... await kernel.browsers.delete_by_id(kernel_browser.session_id) # Second session - load auth kernel_browser2 = await kernel.browsers.create( profile={"name": "my-login"} ) # ... browser now has saved cookies ... ``` ### File Download **Scrapybara** ```python theme={null} instance = client.start_browser() # ... trigger download in browser ... # Then use file operations downloaded_file = instance.file(command="read", path="/download/path") ``` **Kernel** ```python theme={null} kernel_browser = await kernel.browsers.create() # ... trigger download in browser via Playwright ... # Then read from filesystem file_response = await kernel.browsers.fs.read_file( kernel_browser.session_id, path="/tmp/downloads/file.pdf" ) await file_response.write_to_file("local-file.pdf") ``` ### Long-Running Sessions **Scrapybara** ```python theme={null} # Pause/resume for long-running sessions instance = client.start_browser(timeout_hours=24) # ... do some work ... instance.pause() # Pause to save costs # ... later ... instance.resume() # Resume work ``` **Kernel** ```python theme={null} # Automatic standby mode + extended timeout kernel_browser = await kernel.browsers.create( timeout_seconds=86400 ) # ... do some work ... # Browser enters standby mode between connections # Later - reuse the exact browser instance with full state kernel_browser2 = await kernel.browsers.create( timeout_seconds=86400 ) ``` ### Computer Controls Both Scrapybara and Kernel provide Computer Controls APIs that allow you to programmatically control the browser environment at the system level - including mouse movements, clicks, keyboard input, and screenshots. **Scrapybara** ```python theme={null} instance = client.start_browser() # Click at specific coordinates instance.computer(action="click_mouse", button="right", coordinates=[300, 400]) # Drag from one position to another instance.computer(action="drag_mouse", path=[[100, 200], [300, 400]]) # Type text instance.computer(action="type_text", text="Hello World") # Take a screenshot screenshot = instance.computer(action="take_screenshot").base64_image ``` **Kernel** ```python theme={null} kernel_browser = await kernel.browsers.create() # Click at specific coordinates kernel.browsers.computer.click_mouse( id=kernel_browser.session_id, x=100, y=200 ) # Drag from one position to another kernel.browsers.computer.drag_mouse( id=kernel_browser.session_id, path=[[100, 200], [150, 220], [200, 260]], button="left", delay=0, steps_per_segment=10, step_delay_ms=50, hold_keys=["Shift"] ) # Type text with optional delay kernel.browsers.computer.type_text( id=kernel_browser.session_id, text="Hello World", delay=100 ) # Take a full screenshot with open('screenshot.png', 'wb') as f: image_data = kernel.browsers.computer.capture_screenshot(id=kernel_browser.session_id) f.write(image_data.read()) ``` For a complete reference of all available Computer Controls methods in Kernel, see the [Computer Controls documentation](/docs/browsers/computer-controls). ## Full API Comparison | Feature | Scrapybara | Kernel | | ----------------------- | ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- | | **Create Browser** | `client.start_browser()` | `kernel.browsers.create()` | | **Get CDP URL** | `instance.get_cdp_url().cdp_url` | Returns `cdp_ws_url` in create response | | **Get Live View** | `instance.get_stream_url().stream_url` | Returns `browser_live_view_url` in create response | | **Delete Browser** | `instance.stop()` | `kernel.browsers.delete_by_id(session_id)` | | **List Browsers** | `kernel.get_instances()` | `kernel.browsers.list()` | | **Save Auth State** | `instance.browser.save_auth(name="default")` | Create [Profile](/docs/auth/profiles). Then create browser with `kernel.browsers.create(profile={"name": "profile1", "save_changes": True})` | | **Load Auth State** | `instance.browser.authenticate(auth_state_id="xyz")` | `kernel.browsers.create(profile={"name": "profile1"})` | | **Pause/Resume** | `instance.pause()` / `instance.resume()` | Automatic standby mode | | **Screenshot** | `instance.screenshot()` | Use Playwright or Puppeteer | | **Timeout Config** | `timeout_hours` parameter | `timeout_seconds` parameter | | **Stealth Mode** | ❌ Not available | Create browser with `stealth: true` | | **Headless Mode** | ❌ Not available | Create browser with `headless: true` | | **Session Persistence** | Auth state only | Full browser state via creating browser with extended timeout | | **Video Replays** | ❌ Not available | `kernel.browsers.replays.start()` and `kernel.browsers.replays.stop()` | | **File Upload** | `instance.upload()` | `kernel.browsers.fs.upload()` or Playwright | | **File Download** | Via browser, then `instance.file()` | `kernel.browsers.fs.read_file()` | | **Process Control** | `instance.bash()` | `kernel.browsers.process.*` | | **Proxy Support** | ❌ Not available | Create [Proxy](/docs/proxies/overview#create-a-proxy). Then create browser with `kernel.browsers.create(proxy_id=proxy.id)` | | **Click Mouse** | `instance.computer(action="click_mouse", button="left")` | `kernel.browsers.computer.click_mouse(id=session_id, x=100, y=200)` | | **Move Mouse** | `instance.computer(action="move_mouse", coordinates=[100, 200])` | `kernel.browsers.computer.move_mouse(id=session_id, x=100, y=200)` | | **Drag Mouse** | `instance.computer(action="drag_mouse", path=[[100, 200], [300, 400]])` | `kernel.browsers.computer.drag_mouse(id=session_id, path=[[100, 200], [150, 220], [200, 260]])` | | **Scroll** | `instance.computer(action="scroll", coordinates=[100, 100], delta_x=0, delta_y=200)` | `kernel.browsers.computer.scroll(id=session_id, delta_x=0, delta_y=100)` | | **Type Text** | `instance.computer(action="type_text", text="Hello")` | `kernel.browsers.computer.type_text(id=session_id, text="Hello")` | | **Press Key** | `instance.computer(action="press_key", keys=["ctrl", "c"])` | `kernel.browsers.computer.press_key(id=session_id, keys=["Ctrl+t"])` | | **Take Screenshot** | `instance.computer(action="take_screenshot").base64_image` | `kernel.browsers.computer.capture_screenshot(id=session_id)` | | **Get Cursor Position** | `instance.computer(action="get_cursor_position").output` | Use `move_mouse` with tracking | *** ## Need Help? * **Contact Us** on [Discord](https://discord.gg/FBrveQRcud) * **Sign up** [here](https://dashboard.onkernel.com/sign-up) * **Check out** our open source repos [here](https://github.com/onkernel/kernel-images) # Custom Proxies Source: https://kernel.sh/docs/proxies/custom Custom proxies allow you to use your own proxy servers with Kernel browsers. This is useful when you have existing proxy infrastructure or specific proxy requirements not covered by Kernel's managed options. ## Configuration HTTP and HTTPS proxies are supported for custom proxy configurations. If no protocol is specified, HTTPS is used by default. Specify the host, port, and optional authentication credentials for your proxy server: ```typescript Typescript/Javascript theme={null} import { readFileSync } from 'node:fs'; import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); const proxy = await kernel.proxies.create({ type: 'custom', name: 'my-private-proxy', protocol: 'https', config: { host: 'proxy.example.com', port: 443, username: 'user123', password: 'secure_password', ca_bundle: readFileSync('./mitm-ca-bundle.pem', 'utf8'), }, }); const browser = await kernel.browsers.create({ proxy_id: proxy.id, }); ``` ```python Python theme={null} from pathlib import Path from kernel import Kernel kernel = Kernel() proxy = kernel.proxies.create( type="custom", name="my-private-proxy", protocol="https", config={ "host": "proxy.example.com", "port": 443, "username": "user123", "password": "secure_password", "ca_bundle": Path("mitm-ca-bundle.pem").read_text(), } ) browser = kernel.browsers.create(proxy_id=proxy.id) ``` ```go Go theme={null} package main import ( "context" "os" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() caBundle, err := os.ReadFile("mitm-ca-bundle.pem") if err != nil { panic(err) } proxy, err := client.Proxies.New(ctx, kernel.ProxyNewParams{ Type: kernel.ProxyNewParamsTypeCustom, Name: kernel.String("my-private-proxy"), Protocol: kernel.ProxyNewParamsProtocolHTTPS, Config: kernel.ProxyNewParamsConfigUnion{ OfProxyNewsConfigCreateCustomProxyConfig: &kernel.ProxyNewParamsConfigCreateCustomProxyConfig{ Host: "proxy.example.com", Port: 443, Username: kernel.String("user123"), Password: kernel.String("secure_password"), CaBundle: kernel.String(string(caBundle)), }, }, }) if err != nil { panic(err) } browser, err := client.Browsers.New(ctx, kernel.BrowserNewParams{ ProxyID: kernel.String(proxy.ID), }) if err != nil { panic(err) } _ = browser } ``` ## Configuration Parameters * **`host`** (required) - Proxy server hostname or IP address * **`port`** (required) - Proxy server port (1-65535) * **`username`** - Username for proxy authentication * **`password`** - Password for proxy authentication (minimum 5 characters) * **`ca_bundle`** (optional) - PEM-encoded CA certificate bundle to trust when the proxy terminates and re-signs upstream TLS (MITM). May contain multiple certificates and is limited to 64 KiB. * **`bypass_hosts`** (optional) - Array of hostnames that bypass the proxy and connect directly (max 100 entries) When creating a proxy with authentication, provide the password. The API response will only indicate if a password exists (`has_password: true`) but won't return the actual password for security reasons. ## MITM TLS If your custom proxy terminates TLS and re-signs upstream connections, provide its CA certificate bundle in `config.ca_bundle` when you create the proxy. Kernel validates the bundle and installs it in the browser's trust store when you create a browser with that proxy. The bundle must contain one or more PEM-encoded CA certificates. It is write-only: proxy responses return `has_ca_bundle: true` instead of the certificate contents. You must bind a proxy with a CA bundle when creating the browser; you can't hot-swap it onto a running browser. ## Bypass hosts Configure specific hostnames to bypass your custom proxy: ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); const proxy = await kernel.proxies.create({ type: 'custom', name: 'custom-with-bypass', protocol: 'https', config: { host: 'proxy.example.com', port: 443, username: 'user123', password: 'secure_password', }, bypass_hosts: [ 'localhost', 'internal.service.local', '*.trusted-domain.com', ], }); ``` ```python Python theme={null} from kernel import Kernel kernel = Kernel() proxy = kernel.proxies.create( type="custom", name="custom-with-bypass", protocol="https", config={ "host": "proxy.example.com", "port": 443, "username": "user123", "password": "secure_password", }, bypass_hosts=[ "localhost", "internal.service.local", "*.trusted-domain.com", ] ) ``` ```go Go theme={null} package main import ( "context" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() proxy, err := client.Proxies.New(ctx, kernel.ProxyNewParams{ Type: kernel.ProxyNewParamsTypeCustom, Name: kernel.String("custom-with-bypass"), Protocol: kernel.ProxyNewParamsProtocolHTTPS, Config: kernel.ProxyNewParamsConfigUnion{ OfProxyNewsConfigCreateCustomProxyConfig: &kernel.ProxyNewParamsConfigCreateCustomProxyConfig{ Host: "proxy.example.com", Port: 443, Username: kernel.String("user123"), Password: kernel.String("secure_password"), }, }, BypassHosts: []string{ "localhost", "internal.service.local", "*.trusted-domain.com", }, }) if err != nil { panic(err) } _ = proxy } ``` This is useful for accessing internal services or metadata endpoints without routing through your proxy. See the [overview](/docs/proxies/overview#bypass-hosts) for full bypass host rules. # Datacenter Proxies Source: https://kernel.sh/docs/proxies/datacenter Datacenter proxies use IP addresses assigned from datacenter servers to route your traffic and access locations around the world. With a shorter journey and simplified architecture, datacenter proxies are both the fastest and most cost-effective proxy option. ## IP Rotation Behavior Datacenter proxies use **rotating exit IPs** — a new exit IP is assigned per request, so different requests within the same browser session can exit through different IPs. If you need a stable IP across requests and sessions (e.g. for IP allowlists or [managed auth](/docs/auth/overview) health checks), use an [ISP proxy](/docs/proxies/isp) instead. See [IP rotation behavior across proxy types](/docs/proxies/overview) for the full comparison. ## Configuration Datacenter proxies require a country to route traffic through: ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); const proxy = await kernel.proxies.create({ type: 'datacenter', name: 'my-us-datacenter', config: { country: 'US', }, }); const browser = await kernel.browsers.create({ proxy_id: proxy.id, }); ``` ```python Python theme={null} from kernel import Kernel kernel = Kernel() proxy = kernel.proxies.create( type="datacenter", name="my-us-datacenter", config={ "country": "US", } ) browser = kernel.browsers.create(proxy_id=proxy.id) ``` ```go Go theme={null} package main import ( "context" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() proxy, err := client.Proxies.New(ctx, kernel.ProxyNewParams{ Type: kernel.ProxyNewParamsTypeDatacenter, Name: kernel.String("my-us-datacenter"), Config: kernel.ProxyNewParamsConfigUnion{ OfProxyNewsConfigDatacenterProxyConfig: &kernel.ProxyNewParamsConfigDatacenterProxyConfig{ Country: kernel.String("US"), }, }, }) if err != nil { panic(err) } browser, err := client.Browsers.New(ctx, kernel.BrowserNewParams{ ProxyID: kernel.String(proxy.ID), }) if err != nil { panic(err) } _ = browser } ``` ## Configuration Parameters * **`country`** (optional) - ISO 3166 country code (e.g., `US`, `GB`, `FR`) or `EU` for European Union exit nodes * **`bypass_hosts`** (optional) - Array of hostnames that bypass the proxy and connect directly (max 100 entries) ## Bypass hosts Configure specific hostnames to bypass the proxy: ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); const proxy = await kernel.proxies.create({ type: 'datacenter', name: 'datacenter-with-bypass', config: { country: 'US', }, bypass_hosts: [ 'localhost', 'internal.service.local', '*.amazonaws.com', ], }); ``` ```python Python theme={null} from kernel import Kernel kernel = Kernel() proxy = kernel.proxies.create( type="datacenter", name="datacenter-with-bypass", config={ "country": "US", }, bypass_hosts=[ "localhost", "internal.service.local", "*.amazonaws.com", ] ) ``` ```go Go theme={null} package main import ( "context" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() proxy, err := client.Proxies.New(ctx, kernel.ProxyNewParams{ Type: kernel.ProxyNewParamsTypeDatacenter, Name: kernel.String("datacenter-with-bypass"), Config: kernel.ProxyNewParamsConfigUnion{ OfProxyNewsConfigDatacenterProxyConfig: &kernel.ProxyNewParamsConfigDatacenterProxyConfig{ Country: kernel.String("US"), }, }, BypassHosts: []string{ "localhost", "internal.service.local", "*.amazonaws.com", }, }) if err != nil { panic(err) } _ = proxy } ``` Bypass hosts support exact hostnames and wildcard subdomains (`*.example.com`). See the [overview](/docs/proxies/overview#bypass-hosts) for full details. # ISP Proxies Source: https://kernel.sh/docs/proxies/isp ISP (Internet Service Provider) proxies are hosted on datacenter infrastructure but use IP addresses assigned by real residential ISPs. Because the ASN belongs to a residential ISP, target sites see them as residential IPs — while the underlying datacenter hosting gives you the speed and stability you'd expect from a datacenter proxy. ## IP Rotation Behavior ISP proxies provide a **static exit IP that persists across sessions** — every tab, request, reconnection, and future browser session attached to this proxy exits through the same IP. The IP only changes in rare ISP-initiated replacement events. This makes ISP proxies suitable for use cases that require a stable IP, such as IP allowlists or [managed auth](/docs/auth/overview) health checks. For comparison with other proxy types, see [IP rotation behavior across proxy types](/docs/proxies/overview). ## Configuration Create an ISP proxy: ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); const proxy = await kernel.proxies.create({ type: 'isp', name: 'my-isp-proxy', }); const browser = await kernel.browsers.create({ proxy_id: proxy.id, }); ``` ```python Python theme={null} from kernel import Kernel kernel = Kernel() proxy = kernel.proxies.create( type="isp", name="my-isp-proxy", ) browser = kernel.browsers.create(proxy_id=proxy.id) ``` ```go Go theme={null} package main import ( "context" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() proxy, err := client.Proxies.New(ctx, kernel.ProxyNewParams{ Type: kernel.ProxyNewParamsTypeIsp, Name: kernel.String("my-isp-proxy"), }) if err != nil { panic(err) } browser, err := client.Browsers.New(ctx, kernel.BrowserNewParams{ ProxyID: kernel.String(proxy.ID), }) if err != nil { panic(err) } _ = browser } ``` ## Configuration Parameters * **`bypass_hosts`** (optional) - Array of hostnames that bypass the proxy and connect directly (max 100 entries) ## Bypass hosts Configure specific hostnames to bypass the proxy: ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); const proxy = await kernel.proxies.create({ type: 'isp', name: 'isp-with-bypass', bypass_hosts: [ 'localhost', 'internal.service.local', '*.amazonaws.com', ], }); ``` ```python Python theme={null} from kernel import Kernel kernel = Kernel() proxy = kernel.proxies.create( type="isp", name="isp-with-bypass", bypass_hosts=[ "localhost", "internal.service.local", "*.amazonaws.com", ] ) ``` ```go Go theme={null} package main import ( "context" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() proxy, err := client.Proxies.New(ctx, kernel.ProxyNewParams{ Type: kernel.ProxyNewParamsTypeIsp, Name: kernel.String("isp-with-bypass"), BypassHosts: []string{ "localhost", "internal.service.local", "*.amazonaws.com", }, }) if err != nil { panic(err) } _ = proxy } ``` See the [overview](/docs/proxies/overview#bypass-hosts) for full bypass host rules and examples. ## Getting the static IP address Because an ISP proxy has a static exit IP, you can read that IP once and reuse it — for example, to add it to an IP allowlist. The IP is returned as `ip_address` in the response when you [create a proxy](https://kernel.sh/docs/api-reference/proxies/create-a-proxy) and when you [get a proxy by ID](https://kernel.sh/docs/api-reference/proxies/get-proxy-by-id). ```typescript Typescript/Javascript theme={null} const proxy = await kernel.proxies.create({ type: 'isp', name: 'my-isp-proxy', }); console.log(proxy.ip_address); ``` ```python Python theme={null} proxy = kernel.proxies.create( type="isp", name="my-isp-proxy", ) print(proxy.ip_address) ``` ```go Go theme={null} proxy, err := client.Proxies.New(ctx, kernel.ProxyNewParams{ Type: kernel.ProxyNewParamsTypeIsp, Name: kernel.String("my-isp-proxy"), }) if err != nil { panic(err) } fmt.Println(proxy.IPAddress) ``` # Mobile Proxies Source: https://kernel.sh/docs/proxies/mobile Mobile proxies route traffic through mobile carrier networks. Only recommended in advanced stealth use cases. Mobile carrier IPs often route through shared regional gateways. Country targeting is the most reliable option; city and US state targeting are best-effort and may not match every third-party geolocation database. ## Configuration Create a mobile proxy with a target country: ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); const proxy = await kernel.proxies.create({ type: 'mobile', name: 'my-us-mobile', config: { country: 'US' } }); const browser = await kernel.browsers.create({ proxy_id: proxy.id, }); ``` ```python Python theme={null} from kernel import Kernel kernel = Kernel() proxy = kernel.proxies.create( type='mobile', name='my-us-mobile', config={ 'country': 'US' } ) browser = kernel.browsers.create(proxy_id=proxy.id) ``` ## Configuration parameters * **`country`** - ISO 3166 country code. Must be provided when providing other targeting options. * **`state`** - US-only two-letter state code. Best-effort for mobile proxies. * **`city`** - Provider city alias, such as `brooklyn` or `chicago`. Best-effort for mobile proxies. * **`bypass_hosts`** (optional) - Array of hostnames that bypass the proxy and connect directly (max 100 entries) ## Advanced targeting examples ### Target by city Route traffic through a specific city: ```typescript Typescript/Javascript theme={null} const proxy = await kernel.proxies.create({ type: 'mobile', name: 'brooklyn-mobile', config: { country: 'US', state: 'NY', city: 'brooklyn' } }); ``` ```Python Python theme={null} proxy = kernel.proxies.create( type='mobile', name='brooklyn-mobile', config={ 'country': 'US', 'state': 'NY', 'city': 'brooklyn' } ) ``` If the city alias is not matched, the API returns examples from the target state to help you find the correct value. ### Target by state Route traffic through a US state: ```typescript Typescript/Javascript theme={null} const proxy = await kernel.proxies.create({ type: 'mobile', name: 'ny-mobile', config: { country: 'US', state: 'NY' } }); ``` ```Python Python theme={null} proxy = kernel.proxies.create( type='mobile', name='ny-mobile', config={ 'country': 'US', 'state': 'NY' } ) ``` ## Bypass hosts Configure specific hostnames to bypass the proxy: ```typescript Typescript/Javascript theme={null} const proxy = await kernel.proxies.create({ type: 'mobile', name: 'mobile-with-bypass', config: { country: 'US', }, bypass_hosts: [ 'localhost', 'metadata.google.internal', '*.internal.company.com', ], }); ``` ```python Python theme={null} proxy = kernel.proxies.create( type='mobile', name='mobile-with-bypass', config={ 'country': 'US', }, bypass_hosts=[ 'localhost', 'metadata.google.internal', '*.internal.company.com', ] ) ``` See the [overview](/docs/proxies/overview#bypass-hosts) for full bypass host rules and examples. # Overview Source: https://kernel.sh/docs/proxies/overview Kernel proxies enable you to route browser traffic through different types of proxy servers, providing enhanced privacy, flexibility, and bot detection avoidance. Proxies can be created once and reused across multiple browser sessions. ## Proxy Types Kernel supports five types of proxies: 1. [**Datacenter**](/docs/proxies/datacenter) - Traffic routed through commercial data centers 2. [**ISP**](/docs/proxies/isp) - Traffic routed through data centers, using residential IP addresses leased from from internet service providers 3. [**Residential**](/docs/proxies/residential) - Traffic routed through real residential IP addresses 4. [**Mobile**](/docs/proxies/mobile) - Traffic routed through mobile carrier networks 5. [**Custom**](/docs/proxies/custom) - Your own proxy servers Datacenter has the fastest speed, while residential and mobile are least detectable. ISP is a balance between the options, with less-flexible geotargeting. Kernel recommends using the first option in the list that works for your use case. Kernel-provided proxies are unmetered and not billed, subject to the fair use rules and restricted destinations in our [Acceptable Use Policy](/docs/acceptable-use#proxy-fair-use). Some destination categories — including government, banking, and payment domains — are blocked by our upstream network providers and will fail at the proxy layer. Reach them directly or with a [custom proxy](/docs/proxies/custom) instead. ISP proxies provide a **static exit IP that persists across sessions** — every browser session attached to the proxy exits through the same IP, and it only changes in rare ISP-initiated replacement events. This makes them suitable for IP allowlists or [managed auth](/docs/auth/overview) health checks that must egress from a single IP. Datacenter proxies use **rotating exit IPs** — a new exit IP is assigned per request, so different requests within the same browser session can exit through different IPs. For a stable IP across requests and sessions, use an ISP proxy or a [custom (BYO) proxy](/docs/proxies/custom) pointed at infrastructure you control. Residential and mobile proxies use **rotating exit IPs** that may change per connection — see [Residential Proxies](/docs/proxies/residential#ip-rotation-behavior) and [Mobile Proxies](/docs/proxies/mobile) for details. ## Create a proxy Create a proxy configuration from the types above that can be reused across browser sessions: KERNEL automatically deletes proxy configurations that haven't been used for 14 days when your organization has more than 100 active configurations. Configurations attached to sessions, pools, instances, or managed auth connections are retained. ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); const proxy = await kernel.proxies.create({ type: 'datacenter' }); console.log(proxy.id); ``` ```python Python theme={null} from kernel import Kernel kernel = Kernel() proxy = kernel.proxies.create(type="datacenter") print(proxy.id) ``` ```go Go theme={null} package main import ( "context" "fmt" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() proxy, err := client.Proxies.New(ctx, kernel.ProxyNewParams{ Type: kernel.ProxyNewParamsTypeDatacenter, }) if err != nil { panic(err) } fmt.Println(proxy.ID) } ``` ## Rename a proxy Rename a proxy without changing its type or connection settings. Proxy updates are addressed by ID, and proxy names are not unique, so use the proxy ID when a name could match more than one configuration. ```typescript TypeScript theme={null} await kernel.proxies.update(proxy.id, { name: 'checkout-proxy' }); ``` ```python Python theme={null} kernel.proxies.update(proxy.id, name="checkout-proxy") ``` ```go Go theme={null} _, err := client.Proxies.Update(ctx, proxy.ID, kernel.ProxyUpdateParams{ Name: "checkout-proxy", }) ``` ## List your proxies View all proxy configurations in your organization: ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); const proxies = await kernel.proxies.list(); console.log(proxies); ``` ```python Python theme={null} from kernel import Kernel kernel = Kernel() proxies = kernel.proxies.list() print(proxies) ``` ```go Go theme={null} package main import ( "context" "fmt" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() proxies, err := client.Proxies.List(ctx) if err != nil { panic(err) } fmt.Println(proxies) } ``` ## Use with browsers Once created, you can attach a proxy to any browser session using the `proxy_id` parameter: ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); const proxy = await kernel.proxies.create({ type: 'residential', name: 'my-us-residential', config: { country: 'US', }, }); const browser = await kernel.browsers.create({ proxy_id: proxy.id, }); ``` ```python Python theme={null} from kernel import Kernel kernel = Kernel() proxy = kernel.proxies.create( type="residential", name="my-us-residential", config={ "country": "US", } ) browser = kernel.browsers.create(proxy_id=proxy.id) ``` ```go Go theme={null} package main import ( "context" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() proxy, err := client.Proxies.New(ctx, kernel.ProxyNewParams{ Type: kernel.ProxyNewParamsTypeResidential, Name: kernel.String("my-us-residential"), Config: kernel.ProxyNewParamsConfigUnion{ OfProxyNewsConfigResidentialProxyConfig: &kernel.ProxyNewParamsConfigResidentialProxyConfig{ Country: kernel.String("US"), }, }, }) if err != nil { panic(err) } browser, err := client.Browsers.New(ctx, kernel.BrowserNewParams{ ProxyID: kernel.String(proxy.ID), }) if err != nil { panic(err) } _ = browser } ``` ## Check proxy health Before attaching a proxy to a browser session, run a health check to verify credentials and connectivity. Pass an optional `url` to test reachability against a specific target instead of Kernel's default test URLs. ```typescript Typescript/Javascript theme={null} await kernel.proxies.check(proxy.id, { url: 'https://example.com' }); ``` ```python Python theme={null} kernel.proxies.check(id=proxy.id, url="https://example.com") ``` ```go Go theme={null} client.Proxies.Check(ctx, proxy.ID, kernel.ProxyCheckParams{ URL: kernel.String("https://example.com"), }) ``` For ISP and datacenter proxies the exit IP is stable, so a successful check against a `url` reliably indicates that subsequent sessions will reach the same target from the same IP. For residential and mobile proxies the exit node changes between requests, so the check validates credentials and connectivity but not site-specific reachability. When `url` is provided, the result does not update the proxy's stored health status. ## Bypass hosts Configure specific hostnames to bypass the proxy and connect through Kernel-managed direct egress. This is useful for metadata endpoints or reducing latency for trusted domains. To reach a private service through a VPN or tunnel inside the browser session, use [`network.private_hosts`](/docs/browsers/private-networking) instead. Proxy bypass rules don't route traffic into the session's private network. ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); const proxy = await kernel.proxies.create({ type: 'datacenter', name: 'proxy-with-bypass', config: { country: 'US', }, bypass_hosts: [ 'localhost', 'internal.company.local', 'metadata.google.internal', '*.amazonaws.com', ], }); ``` ```python Python theme={null} from kernel import Kernel kernel = Kernel() proxy = kernel.proxies.create( type="datacenter", name="proxy-with-bypass", config={ "country": "US", }, bypass_hosts=[ "localhost", "internal.company.local", "metadata.google.internal", "*.amazonaws.com", ] ) ``` ```go Go theme={null} package main import ( "context" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() proxy, err := client.Proxies.New(ctx, kernel.ProxyNewParams{ Type: kernel.ProxyNewParamsTypeDatacenter, Name: kernel.String("proxy-with-bypass"), Config: kernel.ProxyNewParamsConfigUnion{ OfProxyNewsConfigDatacenterProxyConfig: &kernel.ProxyNewParamsConfigDatacenterProxyConfig{ Country: kernel.String("US"), }, }, BypassHosts: []string{ "localhost", "internal.company.local", "metadata.google.internal", "*.amazonaws.com", }, }) if err != nil { panic(err) } _ = proxy } ``` ### Bypass host rules * **Exact hostnames**: `example.com`, `api.service.local` * **Wildcard subdomains**: `*.example.com` matches `api.example.com`, `cdn.example.com`, etc. * **Maximum 100 entries** per proxy * **Maximum 253 characters** per hostname * Hostnames are case-insensitive and automatically normalized * Ports, paths, and URL schemes are not allowed * IP addresses are not supported—use hostnames only Bypass hosts is available on Start-Up and Enterprise plans. ## Update a browser's proxy You can hot-swap the proxy on a running browser session without restarting it. This updates the proxy configuration immediately — all subsequent network requests from the browser will use the new proxy. The browser's network is momentarily disconnected during a proxy hot swap. Any in-flight requests may fail. ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); // Create two proxy configurations const proxyA = await kernel.proxies.create({ type: 'isp', name: 'proxy-a', config: { country: 'US' }, }); const proxyB = await kernel.proxies.create({ type: 'residential', name: 'proxy-b', config: { country: 'DE' }, }); // Launch a browser with the first proxy const browser = await kernel.browsers.create({ proxy_id: proxyA.id, }); // Hot-swap to a different proxy await kernel.browsers.update(browser.session_id, { proxy_id: proxyB.id, }); // Remove the proxy entirely (route directly to the internet) await kernel.browsers.update(browser.session_id, { proxy_id: '', }); ``` ```python Python theme={null} from kernel import Kernel kernel = Kernel() # Create two proxy configurations proxy_a = kernel.proxies.create( type="isp", name="proxy-a", config={"country": "US"}, ) proxy_b = kernel.proxies.create( type="residential", name="proxy-b", config={"country": "DE"}, ) # Launch a browser with the first proxy browser = kernel.browsers.create( proxy_id=proxy_a.id, ) # Hot-swap to a different proxy kernel.browsers.update( browser.session_id, proxy_id=proxy_b.id, ) # Remove the proxy entirely (route directly to the internet) kernel.browsers.update( browser.session_id, proxy_id="", ) ``` ```go Go theme={null} package main import ( "context" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() // Create two proxy configurations proxyA, err := client.Proxies.New(ctx, kernel.ProxyNewParams{ Type: kernel.ProxyNewParamsTypeIsp, Name: kernel.String("proxy-a"), Config: kernel.ProxyNewParamsConfigUnion{ OfProxyNewsConfigIspProxyConfig: &kernel.ProxyNewParamsConfigIspProxyConfig{ Country: kernel.String("US"), }, }, }) if err != nil { panic(err) } proxyB, err := client.Proxies.New(ctx, kernel.ProxyNewParams{ Type: kernel.ProxyNewParamsTypeResidential, Name: kernel.String("proxy-b"), Config: kernel.ProxyNewParamsConfigUnion{ OfProxyNewsConfigResidentialProxyConfig: &kernel.ProxyNewParamsConfigResidentialProxyConfig{ Country: kernel.String("DE"), }, }, }) if err != nil { panic(err) } // Launch a browser with the first proxy browser, err := client.Browsers.New(ctx, kernel.BrowserNewParams{ ProxyID: kernel.String(proxyA.ID), }) if err != nil { panic(err) } // Hot-swap to a different proxy if _, err := client.Browsers.Update(ctx, browser.SessionID, kernel.BrowserUpdateParams{ ProxyID: kernel.String(proxyB.ID), }); err != nil { panic(err) } // Remove the proxy entirely (route directly to the internet) if _, err := client.Browsers.Update(ctx, browser.SessionID, kernel.BrowserUpdateParams{ ProxyID: kernel.String(""), }); err != nil { panic(err) } } ``` The update is synchronous — when the call returns, the proxy swap is fully applied and all new browser traffic routes through the updated proxy. The swap typically completes in 2–3 seconds. If you swap the proxy on a browser acquired from a browser pool, the browser will be reset back to the browser pool's default proxy configuration when it is released. Releasing the browser will be delayed by the swap duration (\~2-3 seconds) while the proxy is restored to the browser pool default. ### Bring your own proxy Attach a custom `proxy_id` to any browser — stealth or non-stealth — and Kernel's anti-detection config still applies. For full anti-detection without the managed proxy or CAPTCHA solver, launch a non-stealth browser with your own `proxy_id`: If your custom proxy performs TLS interception, provide its CA bundle when creating the proxy. See [MITM TLS](/docs/proxies/custom#mitm-tls) for configuration details. ```typescript Typescript/Javascript theme={null} const browser = await kernel.browsers.create({ stealth: false, proxy_id: myProxy.id, }); ``` ```python Python theme={null} browser = kernel.browsers.create( stealth=False, proxy_id=my_proxy.id, ) ``` ```go Go theme={null} browser, err := client.Browsers.New(ctx, kernel.BrowserNewParams{ Stealth: kernel.Bool(false), ProxyID: kernel.String(myProxy.ID), }) if err != nil { panic(err) } _ = browser ``` ### Disable default proxy on stealth browsers [Stealth browsers](/docs/browsers/bot-detection/stealth) are automatically assigned a proxy. To disable this on a running stealth browser and route traffic directly, set `disable_default_proxy` to `true`: ```typescript Typescript/Javascript theme={null} await kernel.browsers.update(browser.session_id, { disable_default_proxy: true, }); ``` ```python Python theme={null} kernel.browsers.update( browser.session_id, disable_default_proxy=True, ) ``` ```go Go theme={null} if _, err := client.Browsers.Update(ctx, browser.SessionID, kernel.BrowserUpdateParams{ DisableDefaultProxy: kernel.Bool(true), }); err != nil { panic(err) } ``` `disable_default_proxy` can only be used with stealth browsers and cannot be combined with `proxy_id`. ## Delete a proxy When no longer needed, delete the proxy configuration: ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); await kernel.proxies.delete('id'); ``` ```python Python theme={null} from kernel import Kernel kernel = Kernel() kernel.proxies.delete("id") ``` ```go Go theme={null} package main import ( "context" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() if err := client.Proxies.Delete(ctx, "id"); err != nil { panic(err) } } ``` Deleting a proxy immediately reconfigures associated browsers to route directly to the internet. # Residential Proxies Source: https://kernel.sh/docs/proxies/residential Residential proxies route traffic through real residential IP addresses. They support advanced targeting options including city, state, and operating system. Residential proxies use **rotating exit IPs** — each new connection may route through a different residential IP address within your targeted region. This is because residential traffic is routed through real consumer devices that may go offline at any time, so the network assigns a new available exit node per connection. This means different browser tabs or requests to different websites within the same session can show different public IPs. If you need a consistent IP address across all connections, use an [ISP proxy](/docs/proxies/isp) instead. ## IP Rotation Behavior Residential proxies assign a new exit IP for each new TCP connection. In practice: * **Same website across tabs**: Tabs connecting to the same domain typically share a TCP connection (via HTTP connection pooling), so they usually see the same IP. * **Different websites across tabs**: Tabs connecting to different domains open separate connections, so they will likely exit through different residential IPs. * **Reconnections**: If a connection is closed and re-established (e.g., after a timeout or page idle), the new connection may get a different exit IP. This behavior is inherent to residential proxy networks, where traffic is routed through real consumer devices that come online and offline dynamically. ## Configuration Create a residential proxy with a target country: ```typescript Typescript/Javascript theme={null} import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); const proxy = await kernel.proxies.create({ type: 'residential', name: 'my-us-residential', config: { country: 'US' } }); const browser = await kernel.browsers.create({ proxy_id: proxy.id, }); ``` ```python Python theme={null} from kernel import Kernel kernel = Kernel() proxy = kernel.proxies.create( type='residential', name='my-us-residential', config={ 'country': 'US' } ) browser = kernel.browsers.create(proxy_id=proxy.id) ``` ```go Go theme={null} package main import ( "context" "github.com/kernel/kernel-go-sdk" ) func main() { ctx := context.Background() client := kernel.NewClient() proxy, err := client.Proxies.New(ctx, kernel.ProxyNewParams{ Type: kernel.ProxyNewParamsTypeResidential, Name: kernel.String("my-us-residential"), Config: kernel.ProxyNewParamsConfigUnion{ OfProxyNewsConfigResidentialProxyConfig: &kernel.ProxyNewParamsConfigResidentialProxyConfig{ Country: kernel.String("US"), }, }, }) if err != nil { panic(err) } browser, err := client.Browsers.New(ctx, kernel.BrowserNewParams{ ProxyID: kernel.String(proxy.ID), }) if err != nil { panic(err) } _ = browser } ``` ## Configuration Parameters * **`country`** - ISO 3166 country code. Must be provided when providing other targeting options. * **`state`** - Two-letter state code. Only supported for US. * **`city`** - City name (lowercase, no spaces, e.g., `sanfrancisco`, `newyork`). * **`zip`** - US ZIP code (5 digits). Can only be used with `country` set to `US`. Cannot be combined with `city` or `state`. * **`asn`** - Autonomous System Number. Conflicts with city and state. * **`bypass_hosts`** (optional) - Array of hostnames that bypass the proxy and connect directly (max 100 entries) ## Advanced Targeting Examples Kernel recommends using the least-specific targeting configuration that works for your use case. The more specific a configuration, the less available IPs there are, increasing the chance of a slow connection or no available connection (`no_peer` connection error). ### Target by City Route traffic through a specific city: ```typescript Typescript/Javascript theme={null} const proxy = await kernel.proxies.create({ type: 'residential', name: 'la-residential', config: { country: 'US', state: 'CA', city: 'los_angeles' } }); ``` ```Python Python theme={null} proxy = kernel.proxies.create( type='residential', name='la-residential', config={ 'country': 'US', 'state': 'CA', 'city': 'los_angeles' } ) ``` ```go Go theme={null} proxy, err := client.Proxies.New(ctx, kernel.ProxyNewParams{ Type: kernel.ProxyNewParamsTypeResidential, Name: kernel.String("la-residential"), Config: kernel.ProxyNewParamsConfigUnion{ OfProxyNewsConfigResidentialProxyConfig: &kernel.ProxyNewParamsConfigResidentialProxyConfig{ Country: kernel.String("US"), State: kernel.String("CA"), City: kernel.String("los_angeles"), }, }, }) if err != nil { panic(err) } _ = proxy ``` If the city name is not matched, the API will return the best 10 city names from the state to help you find the correct city identifier. ### Target by State Route traffic through a specific state: ```typescript Typescript/Javascript theme={null} const proxy = await kernel.proxies.create({ type: 'residential', name: 'ny-residential', config: { country: 'US', state: 'NY' } }); ``` ```Python Python theme={null} proxy = kernel.proxies.create( type='residential', name='ny-residential', config={ 'country': 'US', 'state': 'NY' } ) ``` ```go Go theme={null} proxy, err := client.Proxies.New(ctx, kernel.ProxyNewParams{ Type: kernel.ProxyNewParamsTypeResidential, Name: kernel.String("ny-residential"), Config: kernel.ProxyNewParamsConfigUnion{ OfProxyNewsConfigResidentialProxyConfig: &kernel.ProxyNewParamsConfigResidentialProxyConfig{ Country: kernel.String("US"), State: kernel.String("NY"), }, }, }) if err != nil { panic(err) } _ = proxy ``` If the state name is not matched, the API will return the most-available 10 states. ### Target by ASN Route traffic through a specific Autonomous System Number (ISP): ```typescript Typescript/Javascript theme={null} const proxy = await kernel.proxies.create({ type: 'residential', name: 'comcast-residential', config: { country: 'US', asn: 'AS7922' } }); ``` ```Python Python theme={null} proxy = kernel.proxies.create( type='residential', name='comcast-residential', config={ 'country': 'US', 'asn': 'AS7922' } ) ``` ```go Go theme={null} proxy, err := client.Proxies.New(ctx, kernel.ProxyNewParams{ Type: kernel.ProxyNewParamsTypeResidential, Name: kernel.String("comcast-residential"), Config: kernel.ProxyNewParamsConfigUnion{ OfProxyNewsConfigResidentialProxyConfig: &kernel.ProxyNewParamsConfigResidentialProxyConfig{ Country: kernel.String("US"), Asn: kernel.String("AS7922"), }, }, }) if err != nil { panic(err) } _ = proxy ``` If the ASN is not matched, the API will return the most-available 10 examples. ### Target by ZIP code Route traffic through a specific US ZIP code area: ```typescript Typescript/Javascript theme={null} const proxy = await kernel.proxies.create({ type: 'residential', name: 'nyc-residential', config: { country: 'US', zip: '10001' } }); ``` ```Python Python theme={null} proxy = kernel.proxies.create( type='residential', name='nyc-residential', config={ 'country': 'US', 'zip': '10001' } ) ``` ```go Go theme={null} proxy, err := client.Proxies.New(ctx, kernel.ProxyNewParams{ Type: kernel.ProxyNewParamsTypeResidential, Name: kernel.String("nyc-residential"), Config: kernel.ProxyNewParamsConfigUnion{ OfProxyNewsConfigResidentialProxyConfig: &kernel.ProxyNewParamsConfigResidentialProxyConfig{ Country: kernel.String("US"), Zip: kernel.String("10001"), }, }, }) if err != nil { panic(err) } _ = proxy ``` ZIP code targeting is US-only and cannot be combined with `state` or `city`. The exit IP will be in the geographic area of the requested ZIP code, but the IP's exact ZIP may differ slightly (e.g., requesting `90210` may route through `90401` in the same metro area). Not all ZIP codes have available residential IPs. If a ZIP code is unavailable, proxy creation will fail with a message suggesting alternatives: a nearby ZIP, city/state targeting, or country-only targeting. ## Bypass hosts Configure specific hostnames to bypass the proxy and connect directly: ```typescript Typescript/Javascript theme={null} const proxy = await kernel.proxies.create({ type: 'residential', name: 'residential-with-bypass', config: { country: 'US', }, bypass_hosts: [ 'localhost', 'metadata.google.internal', '*.internal.company.com', ], }); ``` ```python Python theme={null} proxy = kernel.proxies.create( type='residential', name='residential-with-bypass', config={ 'country': 'US', }, bypass_hosts=[ 'localhost', 'metadata.google.internal', '*.internal.company.com', ] ) ``` ```go Go theme={null} proxy, err := client.Proxies.New(ctx, kernel.ProxyNewParams{ Type: kernel.ProxyNewParamsTypeResidential, Name: kernel.String("residential-with-bypass"), Config: kernel.ProxyNewParamsConfigUnion{ OfProxyNewsConfigResidentialProxyConfig: &kernel.ProxyNewParamsConfigResidentialProxyConfig{ Country: kernel.String("US"), }, }, BypassHosts: []string{ "localhost", "metadata.google.internal", "*.internal.company.com", }, }) if err != nil { panic(err) } _ = proxy ``` See the [overview](/docs/proxies/overview#bypass-hosts) for full bypass host rules and examples. # Kernel CLI Source: https://kernel.sh/docs/reference/cli The Kernel CLI helps you access and manage your Kernel resources. ## Installation ```bash theme={null} # Using brew brew install kernel/tap/kernel # Using pnpm pnpm install -g @onkernel/cli # Using npm npm install -g @onkernel/cli ``` Verify installation: ```bash theme={null} which kernel kernel --version ``` Scaffold new Kernel apps from templates. Login, logout, and check auth status. Create, view, and manage Kernel browsers. Pre-provision and reuse browser pools. Persist and reuse browser profiles. Create and manage proxy configurations. Deploy apps, invoke actions, and stream logs. Manage auth connections, credentials, and credential providers. Upload, download, and build browser extensions. Install Kernel MCP server configuration for AI tools. Manage projects and scope commands with `--project`. Create, inspect, rotate, rename, and delete API keys. Inspect effective entitlements and configure concurrency limits. Search and download organization audit logs. ## Quick Start ```bash theme={null} # 1) Create a new app kernel create # 2) Login kernel login # 3) Deploy your app kernel deploy index.ts # 4) Invoke your app kernel invoke my-app action-name --payload '{"key":"value"}' ``` ## Global Flags * `--version`, `-v` - Print the CLI version * `--no-color` - Disable color output * `--log-level ` - Set the log level (trace, debug, info, warn, error, fatal, print) * `--project ` - Scope the request to a specific [project](/docs/reference/cli/projects) ID (also reads the `KERNEL_PROJECT` env var) ## JSON Output Many commands support `--output json` (or `-o json`) for machine-readable output, useful for scripting and automation: ```bash theme={null} # Get browser session as JSON kernel browsers create -o json # List apps as JSON array kernel app list -o json # Deploy with JSONL streaming (one JSON object per line) kernel deploy index.ts -o json ``` See individual command documentation for JSON output availability. ## Utility Commands ### `kernel status` Check the operational status of Kernel services. * `--output json`, `-o json` - Output raw JSON object. ### `kernel upgrade` Upgrade the Kernel CLI to the latest version. * `--dry-run` - Show what would be executed without running it. ### `kernel completion ` Generate a shell autocompletion script (`bash`, `zsh`, `fish`, or `powershell`). ```bash theme={null} # Load completions for the current zsh session source <(kernel completion zsh) ``` Looking for the API? See the [API Reference](https://kernel.sh/docs/api-reference/invocations/invoke-an-action). ## Skills The Kernel CLI has a companion [skills file](https://github.com/kernel/skills/blob/main/plugins/kernel-cli/skills/kernel-cli/SKILL.md) that AI coding agents (Cursor, Claude, Windsurf, etc.) can use to learn the CLI's commands and capabilities. Copy the prompt from the [homepage](/docs/) or point your agent directly at the skill file to get started. # API Keys Source: https://kernel.sh/docs/reference/cli/api-keys Manage [API keys](/docs/info/api-keys) from the CLI. ## `kernel api-keys create` Create an API key. By default, the new key is org-wide. Pass `--project-id` to create a key whose own access is scoped to that project. ```bash theme={null} kernel api-keys create \ --name staging-ci \ --days-to-expire 30 \ --project-id proj_staging_9f3k \ --output json ``` | Flag | Description | | --------------------------- | -------------------------------------------------------------------- | | `--name ` | API key name. Required. | | `--days-to-expire ` | Number of days until expiry, from `1` to `3650`. Omit for no expiry. | | `--project-id ` | Create a project-scoped API key for this project. Omit for org-wide. | | `--output json`, `-o json` | Output the raw JSON object, including the plaintext `key` on create. | `--project-id` controls the access scope of the new API key. The global `--project` flag only scopes the CLI request you're making. ## `kernel api-keys list` List API keys in the authenticated organization. API keys are masked. ```bash theme={null} kernel api-keys list --limit 20 ``` | Flag | Description | | -------------------------- | ------------------------------------ | | `--limit ` | Maximum number of results to return. | | `--offset ` | Number of results to skip. | | `--output json`, `-o json` | Output the raw JSON array. | ## `kernel api-keys get ` Show one API key by ID. The response includes the masked key and metadata, not the plaintext key. ```bash theme={null} kernel api-keys get key_01jwv4tn5m8k3q2v7x9p0a1bc2 ``` | Flag | Description | | -------------------------- | -------------------------------------------- | | `--include-deleted` | Include soft-deleted API keys in the lookup. | | `--output json`, `-o json` | Output the raw JSON object. | ## `kernel api-keys update ` Rename an API key. ```bash theme={null} kernel api-keys update key_01jwv4tn5m8k3q2v7x9p0a1bc2 --name staging-ci-rotated ``` | Flag | Description | | -------------------------- | --------------------------- | | `--name ` | New API key name. Required. | | `--output json`, `-o json` | Output the raw JSON object. | ## `kernel api-keys rotate ` Issue a replacement API key. The rotated key keeps working for a grace period so you can migrate callers without downtime. ```bash theme={null} kernel api-keys rotate key_01jwv4tn5m8k3q2v7x9p0a1bc2 \ --days-to-expire 90 \ --expire-in-days 7 \ --yes \ --output json ``` | Flag | Description | | -------------------------- | --------------------------------------------------------------------------------------------------------------- | | `--days-to-expire ` | Lifetime of the new key, from `1` to `3650` days. Omit to reuse the rotated key's lifetime. | | `--expire-in-days ` | Grace period before the rotated key expires. Pass `0` to expire it immediately; omit for the default of 7 days. | | `--yes`, `-y` | Skip the confirmation prompt. | | `--output json`, `-o json` | Output the raw JSON object, including the new one-time plaintext key. | Save the new plaintext key when you rotate it. Kernel won't return it again. ## `kernel api-keys delete ` Delete an API key. ```bash theme={null} kernel api-keys delete key_01jwv4tn5m8k3q2v7x9p0a1bc2 --yes ``` | Flag | Description | | ------------- | ----------------------------- | | `--yes`, `-y` | Skip the confirmation prompt. | ## Aliases You can also use `kernel api-key`, `kernel apikeys`, or `kernel apikey`. # Apps Source: https://kernel.sh/docs/reference/cli/apps ## `kernel deploy ` Deploy an app to Kernel from the current directory. The entrypoint file and dependency manifest must live in the project root. | Flag | Description | | -------------------------- | ------------------------------------------------------------------ | | `--version ` | Use a specific version label (default: latest). | | `--force` | Overwrite an existing version with the same label. | | `--env `, `-e` | Set environment variables (repeatable). | | `--env-file ` | Load environment variables from a file (repeatable). | | `--output json`, `-o json` | Output JSONL (one JSON object per line for each deployment event). | `package.json` (JS/TS) or `pyproject.toml` (Python) must be present next to the entrypoint. ## `kernel deploy github` Deploy directly from a GitHub repository without cloning or pushing locally. | Flag | Description | | ------------------------ | ------------------------------------------------------------------------- | | `--url ` | GitHub repository URL (e.g. `https://github.com/org/repo`). | | `--ref ` | Git ref to deploy (branch, tag, or commit SHA). | | `--entrypoint ` | Entrypoint within the repo (e.g. `src/index.ts`). | | `--path ` | Subdirectory within the repo (e.g. `apps/api`). | | `--github-token ` | GitHub token for private repositories (PAT or installation access token). | | `--region ` | Deployment region (currently only `aws.us-east-1a`). | For private repositories, provide `--github-token` or set the `GITHUB_TOKEN` environment variable. ## `kernel deploy logs ` Stream build and runtime logs for a deployment. | Flag | Description | | -------------------------- | ---------------------------------------------------------------------------------------------------------- | | `--follow`, `-f` | Continue streaming logs in real time. | | `--since `, `-s` | Fetch logs starting from a relative duration (e.g. `5m`, `1h`, `1h30m`) or timestamp (`2006-01-02T15:04`). | | `--with-timestamps`, `-t` | Prefix each line with an RFC3339 timestamp. | Log lines longer than 64 KiB are truncated. Emit bulky payloads to external storage and log references. ## `kernel deploy history [app_name]` Show deployment history for all apps or a specific app. | Flag | Description | | -------------------------- | ------------------------------------------------------------------ | | `--limit ` | Maximum number of deployments to return (default: 100, `0` = all). | | `--output json`, `-o json` | Output raw JSON array. | ## `kernel deploy get ` Get details for a deployment. | Flag | Description | | -------------------------- | ----------------------- | | `--output json`, `-o json` | Output raw JSON object. | ## `kernel deploy delete ` Delete a deployment. | Flag | Description | | ------------- | ------------------------- | | `--yes`, `-y` | Skip confirmation prompt. | ## `kernel invoke ` Invoke an app action. By default the CLI returns immediately after the invocation is queued. | Flag | Description | | ----------------------------- | ----------------------------------------------------------------------------- | | `--version `, `-v` | Target a specific app version (default: latest). | | `--payload `, `-p` | Provide a JSON payload (stringified, max 64 KB). | | `--payload-file `, `-f` | Read the JSON payload from a file (use `-` for stdin). | | `--sync`, `-s` | Wait for completion (timeout after 60 s). | | `--async-timeout ` | Timeout for async invocations (min 10, max 3600). Only applies in async mode. | | `--since