Repository navigation
feat(adk): consume shared @wave-av/kernel client (Phase A consumer 1) #35
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| import { describe, it, expect, vi, beforeEach } from 'vitest'; | ||
|
|
||
| // Mock the shared Kernel client so the adapter's lifecycle (create → playwright | ||
| // execute → deleteByID) is observable without a live Kernel API key. | ||
| const create = vi.fn(); | ||
| const execute = vi.fn(); | ||
| const deleteByID = vi.fn().mockResolvedValue(undefined); | ||
|
|
||
| vi.mock('@wave-av/kernel', async () => { | ||
| const actual = await vi.importActual<typeof import('@wave-av/kernel')>('@wave-av/kernel'); | ||
| return { | ||
| ...actual, | ||
| WaveKernel: class { | ||
| browsers = { | ||
| create, | ||
| deleteByID, | ||
| playwright: { execute }, | ||
| }; | ||
| }, | ||
| }; | ||
| }); | ||
|
|
||
| async function tools() { | ||
| const { createKernelTools } = await import('../adapters/kernel'); | ||
| return createKernelTools({ apiKey: 'test-key' }); | ||
| } | ||
|
|
||
| function tool(name: string) { | ||
| return tools().then((ts) => ts.find((t) => t.name === name)!); | ||
| } | ||
|
|
||
| /** The `code` string of the nth (0-indexed) `playwright.execute` call. */ | ||
| function executedCode(nth: number): string { | ||
| return (execute.mock.calls[nth][1] as { code: string }).code; | ||
| } | ||
|
|
||
| beforeEach(() => { | ||
| create.mockReset().mockResolvedValue({ session_id: 'br_123', cdp_ws_url: 'ws://x' }); | ||
| execute.mockReset(); | ||
| deleteByID.mockReset().mockResolvedValue(undefined); | ||
| }); | ||
|
|
||
| describe('Kernel adapter — lifecycle', () => { | ||
| it('browse_url creates a session, runs goto/title/content code, then tears it down', async () => { | ||
| execute.mockResolvedValue({ success: true, result: { title: 'T', content: '<html>' } }); | ||
| const browse = await tool('browse_url'); | ||
|
|
||
| const out = await browse.handler({ url: 'https://example.com' }); | ||
|
|
||
| expect(create).toHaveBeenCalledOnce(); | ||
| expect(execute).toHaveBeenCalledWith('br_123', { code: expect.stringContaining('page.goto') }); | ||
| expect(executedCode(0)).toContain('page.title'); | ||
| expect(deleteByID).toHaveBeenCalledWith('br_123'); | ||
| expect(out).toEqual({ title: 'T', content: '<html>' }); | ||
| }); | ||
|
|
||
| it('waitForSelector is injected into the executed code when provided', async () => { | ||
| execute.mockResolvedValue({ success: true, result: {} }); | ||
| const browse = await tool('browse_url'); | ||
|
|
||
| await browse.handler({ url: 'https://example.com', waitForSelector: '#player' }); | ||
|
|
||
| expect(executedCode(0)).toContain('waitForSelector'); | ||
| expect(executedCode(0)).toContain('#player'); | ||
| }); | ||
|
|
||
| it('take_screenshot returns base64 and uses fullPage when no selector', async () => { | ||
| execute.mockResolvedValue({ success: true, result: { screenshotBase64: 'AAAA', width: 800, height: 600 } }); | ||
| const shot = await tool('take_screenshot'); | ||
|
|
||
| const out = await shot.handler({ url: 'https://example.com', width: 800, height: 600 }); | ||
|
|
||
| expect(executedCode(0)).toContain('fullPage: true'); | ||
| expect(out).toEqual({ screenshotBase64: 'AAAA', width: 800, height: 600 }); | ||
| }); | ||
|
|
||
| it('take_screenshot scopes to a locator (no fullPage) when a selector is given', async () => { | ||
| execute.mockResolvedValue({ success: true, result: {} }); | ||
| const shot = await tool('take_screenshot'); | ||
|
|
||
| await shot.handler({ url: 'https://example.com', selector: '.video' }); | ||
|
|
||
| const code = executedCode(0); | ||
| expect(code).toContain('page.locator'); | ||
| expect(code).not.toContain('fullPage'); | ||
| }); | ||
|
|
||
| it('run_playwright navigates first when a url is given, then runs the user code', async () => { | ||
| execute.mockResolvedValue({ success: true, result: 'done' }); | ||
| const run = await tool('run_playwright'); | ||
|
|
||
| const out = await run.handler({ code: 'return await page.title();', url: 'https://example.com' }); | ||
|
|
||
| expect(executedCode(0)).toContain('page.goto'); | ||
| expect(execute).toHaveBeenNthCalledWith(2, 'br_123', { code: 'return await page.title();' }); | ||
| expect(out).toEqual({ output: 'done', logs: [] }); | ||
| }); | ||
|
|
||
| it('run_playwright surfaces stdout/stderr as logs', async () => { | ||
| execute.mockResolvedValue({ success: true, result: 'ok', stdout: 'hello', stderr: 'warn' }); | ||
| const run = await tool('run_playwright'); | ||
|
|
||
| const out = await run.handler({ code: 'return 1;' }); | ||
|
|
||
| expect(out).toEqual({ output: 'ok', logs: ['hello', 'warn'] }); | ||
| }); | ||
|
|
||
| it('a non-success execution surfaces a KernelApiError AND still tears the session down', async () => { | ||
| const { KernelApiError } = await import('@wave-av/kernel'); | ||
| execute.mockResolvedValue({ success: false, error: 'boom' }); | ||
| const browse = await tool('browse_url'); | ||
|
|
||
| await expect(browse.handler({ url: 'https://example.com' })).rejects.toBeInstanceOf(KernelApiError); | ||
| expect(deleteByID).toHaveBeenCalledWith('br_123'); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,28 +1,42 @@ | ||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||
| * Kernel.sh Adapter for WAVE ADK | ||||||||||||||||||||||||||||||||||||||
| * Kernel Adapter for WAVE ADK | ||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||
| * Provides cloud browser capabilities to WAVE agents via Kernel.sh: | ||||||||||||||||||||||||||||||||||||||
| * Provides cloud browser capabilities to WAVE agents via Kernel: | ||||||||||||||||||||||||||||||||||||||
| * - Screenshot live stream players for visual QA | ||||||||||||||||||||||||||||||||||||||
| * - Interact with third-party dashboards (Mux, Cloudflare) | ||||||||||||||||||||||||||||||||||||||
| * - Run visual regression on embedded players | ||||||||||||||||||||||||||||||||||||||
| * - Scrape competitor streaming pages for intelligence | ||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||
| * Kernel provides managed cloud browsers (unikernel-based) | ||||||||||||||||||||||||||||||||||||||
| * with Playwright API, session persistence, and file I/O. | ||||||||||||||||||||||||||||||||||||||
| * Kernel provides managed cloud browsers (unikernel-based) with a Playwright | ||||||||||||||||||||||||||||||||||||||
| * execution surface, session persistence, and file I/O. | ||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||
| * All Kernel access goes through the shared {@link WaveKernel} client | ||||||||||||||||||||||||||||||||||||||
| * (`@wave-av/kernel`), never a hand-rolled fetch — one place for telemetry, | ||||||||||||||||||||||||||||||||||||||
| * resilience, auth, and the SDK's real resource surface | ||||||||||||||||||||||||||||||||||||||
| * (law: kernel-substrate-governed). | ||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||
| * @see https://docs.onkernel.com | ||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| import { WaveKernel, KernelApiError } from '@wave-av/kernel'; | ||||||||||||||||||||||||||||||||||||||
| import { z } from 'zod'; | ||||||||||||||||||||||||||||||||||||||
| import type { AgentTool } from '../tools/AgentToolkit.js'; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| export interface KernelConfig { | ||||||||||||||||||||||||||||||||||||||
| readonly apiKey: string; | ||||||||||||||||||||||||||||||||||||||
| /** Override the Kernel API base URL. Defaults to the SDK default (`https://api.onkernel.com/`). */ | ||||||||||||||||||||||||||||||||||||||
| readonly baseUrl?: string; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const DEFAULT_TIMEOUT_MS = 30_000; | ||||||||||||||||||||||||||||||||||||||
| const DEFAULT_WIDTH = 1920; | ||||||||||||||||||||||||||||||||||||||
| const DEFAULT_HEIGHT = 1080; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||
| * Create WAVE ADK tools backed by Kernel.sh cloud browsers. | ||||||||||||||||||||||||||||||||||||||
| * Create WAVE ADK tools backed by Kernel cloud browsers. | ||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||
| * Each tool spins up a fresh Kernel browser session, drives it through the | ||||||||||||||||||||||||||||||||||||||
| * SDK's Playwright execution surface, and always tears the session down. | ||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||
| * Usage: | ||||||||||||||||||||||||||||||||||||||
| * ```typescript | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -34,84 +48,152 @@ export interface KernelConfig { | |||||||||||||||||||||||||||||||||||||
| * ``` | ||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||
| export function createKernelTools(config: KernelConfig): AgentTool[] { | ||||||||||||||||||||||||||||||||||||||
| const baseUrl = config.baseUrl ?? 'https://api.onkernel.com'; | ||||||||||||||||||||||||||||||||||||||
| const kernel = new WaveKernel({ | ||||||||||||||||||||||||||||||||||||||
| apiKey: config.apiKey, | ||||||||||||||||||||||||||||||||||||||
| ...(config.baseUrl !== undefined ? { baseURL: config.baseUrl } : {}), | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| async function kernelFetch<T>(path: string, body: Record<string, unknown>): Promise<T> { | ||||||||||||||||||||||||||||||||||||||
| const response = await fetch(`${baseUrl}${path}`, { | ||||||||||||||||||||||||||||||||||||||
| method: 'POST', | ||||||||||||||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||||||||||||||
| 'Authorization': `Bearer ${config.apiKey}`, | ||||||||||||||||||||||||||||||||||||||
| 'Content-Type': 'application/json', | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| body: JSON.stringify(body), | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if (!response.ok) { | ||||||||||||||||||||||||||||||||||||||
| throw new Error(`Kernel API error: ${response.status} ${response.statusText}`); | ||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||
| * Create a fresh cloud browser, run `fn` against its session id, and always | ||||||||||||||||||||||||||||||||||||||
| * terminate the session afterwards (even if `fn` throws). Teardown failures | ||||||||||||||||||||||||||||||||||||||
| * are swallowed so they never mask the real result/error. | ||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||
| async function withBrowser<T>(fn: (browserId: string) => Promise<T>): Promise<T> { | ||||||||||||||||||||||||||||||||||||||
| const browser = await kernel.browsers.create({}); | ||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||
| return await fn(browser.session_id); | ||||||||||||||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||||||||||||||
| await kernel.browsers.deleteByID(browser.session_id).catch(() => {}); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+61
to
+68
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win Teardown failures are swallowed with no observability.
♻️ Suggested stopgap logging } finally {
- await kernel.browsers.deleteByID(browser.session_id).catch(() => {});
+ await kernel.browsers.deleteByID(browser.session_id).catch((err) => {
+ console.error(`Failed to tear down Kernel browser ${browser.session_id}:`, err);
+ });
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| return response.json() as Promise<T>; | ||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||
| * Execute Playwright code in the browser VM. The code has access to `page`, | ||||||||||||||||||||||||||||||||||||||
| * `context`, and `browser`, and returns via `return`. A non-success result is | ||||||||||||||||||||||||||||||||||||||
| * surfaced through the shared Kernel error taxonomy. Returns the full | ||||||||||||||||||||||||||||||||||||||
| * execution response so callers can read `result`, `stdout`, and `stderr`. | ||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||
| async function execute(browserId: string, code: string) { | ||||||||||||||||||||||||||||||||||||||
| const res = await kernel.browsers.playwright.execute(browserId, { code }); | ||||||||||||||||||||||||||||||||||||||
| if (!res.success) { | ||||||||||||||||||||||||||||||||||||||
| throw new KernelApiError(res.error ?? 'Playwright execution failed', undefined, { | ||||||||||||||||||||||||||||||||||||||
| browserId, | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| return res; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const browseSchema = z.object({ url: z.string().url(), waitForSelector: z.string().optional(), timeoutMs: z.number().optional() }); | ||||||||||||||||||||||||||||||||||||||
| const screenshotSchema = z.object({ url: z.string().url(), selector: z.string().optional(), width: z.number().optional(), height: z.number().optional() }); | ||||||||||||||||||||||||||||||||||||||
| const browseSchema = z.object({ | ||||||||||||||||||||||||||||||||||||||
| url: z.string().url(), | ||||||||||||||||||||||||||||||||||||||
| waitForSelector: z.string().optional(), | ||||||||||||||||||||||||||||||||||||||
| timeoutMs: z.number().optional(), | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
| const screenshotSchema = z.object({ | ||||||||||||||||||||||||||||||||||||||
| url: z.string().url(), | ||||||||||||||||||||||||||||||||||||||
| selector: z.string().optional(), | ||||||||||||||||||||||||||||||||||||||
| width: z.number().optional(), | ||||||||||||||||||||||||||||||||||||||
| height: z.number().optional(), | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
| const playwrightSchema = z.object({ code: z.string().min(1), url: z.string().url().optional() }); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| return [ | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| name: 'browse_url', | ||||||||||||||||||||||||||||||||||||||
| description: 'Navigate a cloud browser to a URL and return the page content/snapshot. Uses Kernel.sh managed browsers — no local browser needed.', | ||||||||||||||||||||||||||||||||||||||
| description: | ||||||||||||||||||||||||||||||||||||||
| 'Navigate a cloud browser to a URL and return the page title + HTML content. Uses Kernel managed browsers — no local browser needed.', | ||||||||||||||||||||||||||||||||||||||
| parameters: { | ||||||||||||||||||||||||||||||||||||||
| url: { type: 'string', description: 'URL to navigate to', required: true }, | ||||||||||||||||||||||||||||||||||||||
| waitForSelector: { type: 'string', description: 'CSS selector to wait for before capturing', required: false }, | ||||||||||||||||||||||||||||||||||||||
| timeoutMs: { type: 'number', description: 'Navigation timeout in milliseconds', required: false }, | ||||||||||||||||||||||||||||||||||||||
| waitForSelector: { | ||||||||||||||||||||||||||||||||||||||
| type: 'string', | ||||||||||||||||||||||||||||||||||||||
| description: 'CSS selector to wait for before capturing', | ||||||||||||||||||||||||||||||||||||||
| required: false, | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| timeoutMs: { | ||||||||||||||||||||||||||||||||||||||
| type: 'number', | ||||||||||||||||||||||||||||||||||||||
| description: 'Navigation timeout in milliseconds', | ||||||||||||||||||||||||||||||||||||||
| required: false, | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| schema: browseSchema, | ||||||||||||||||||||||||||||||||||||||
| handler: async (params: Record<string, unknown>) => { | ||||||||||||||||||||||||||||||||||||||
| const result = await kernelFetch<{ browserId: string; content: string; title: string }>('/v1/browsers/navigate', { | ||||||||||||||||||||||||||||||||||||||
| url: params.url, | ||||||||||||||||||||||||||||||||||||||
| waitForSelector: params.waitForSelector, | ||||||||||||||||||||||||||||||||||||||
| timeout: params.timeoutMs ?? 30_000, | ||||||||||||||||||||||||||||||||||||||
| const p = browseSchema.parse(params); | ||||||||||||||||||||||||||||||||||||||
| const timeout = p.timeoutMs ?? DEFAULT_TIMEOUT_MS; | ||||||||||||||||||||||||||||||||||||||
| return withBrowser(async (id) => { | ||||||||||||||||||||||||||||||||||||||
| const waitForSelector = p.waitForSelector | ||||||||||||||||||||||||||||||||||||||
| ? `await page.waitForSelector(${JSON.stringify(p.waitForSelector)}, { timeout: ${timeout} });` | ||||||||||||||||||||||||||||||||||||||
| : ''; | ||||||||||||||||||||||||||||||||||||||
| const code = [ | ||||||||||||||||||||||||||||||||||||||
| `await page.goto(${JSON.stringify(p.url)}, { waitUntil: 'load', timeout: ${timeout} });`, | ||||||||||||||||||||||||||||||||||||||
| waitForSelector, | ||||||||||||||||||||||||||||||||||||||
| `return { title: await page.title(), content: await page.content() };`, | ||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||
| .filter(Boolean) | ||||||||||||||||||||||||||||||||||||||
| .join('\n'); | ||||||||||||||||||||||||||||||||||||||
| return (await execute(id, code)).result as { title: string; content: string }; | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
| return result; | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| name: 'take_screenshot', | ||||||||||||||||||||||||||||||||||||||
| description: 'Take a screenshot of a URL using a cloud browser. Returns base64-encoded PNG. Useful for visual QA of live stream players.', | ||||||||||||||||||||||||||||||||||||||
| description: | ||||||||||||||||||||||||||||||||||||||
| 'Take a screenshot of a URL using a cloud browser. Returns a base64-encoded PNG. Useful for visual QA of live stream players.', | ||||||||||||||||||||||||||||||||||||||
| parameters: { | ||||||||||||||||||||||||||||||||||||||
| url: { type: 'string', description: 'URL to screenshot', required: true }, | ||||||||||||||||||||||||||||||||||||||
| selector: { type: 'string', description: 'CSS selector to screenshot (optional, defaults to full page)', required: false }, | ||||||||||||||||||||||||||||||||||||||
| selector: { | ||||||||||||||||||||||||||||||||||||||
| type: 'string', | ||||||||||||||||||||||||||||||||||||||
| description: 'CSS selector to screenshot (optional, defaults to full page)', | ||||||||||||||||||||||||||||||||||||||
| required: false, | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| width: { type: 'number', description: 'Viewport width in pixels', required: false }, | ||||||||||||||||||||||||||||||||||||||
| height: { type: 'number', description: 'Viewport height in pixels', required: false }, | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| schema: screenshotSchema, | ||||||||||||||||||||||||||||||||||||||
| handler: async (params: Record<string, unknown>) => { | ||||||||||||||||||||||||||||||||||||||
| const result = await kernelFetch<{ screenshotBase64: string; width: number; height: number }>('/v1/browsers/screenshot', { | ||||||||||||||||||||||||||||||||||||||
| url: params.url, | ||||||||||||||||||||||||||||||||||||||
| selector: params.selector, | ||||||||||||||||||||||||||||||||||||||
| viewport: { | ||||||||||||||||||||||||||||||||||||||
| width: params.width ?? 1920, | ||||||||||||||||||||||||||||||||||||||
| height: params.height ?? 1080, | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| const p = screenshotSchema.parse(params); | ||||||||||||||||||||||||||||||||||||||
| const width = p.width ?? DEFAULT_WIDTH; | ||||||||||||||||||||||||||||||||||||||
| const height = p.height ?? DEFAULT_HEIGHT; | ||||||||||||||||||||||||||||||||||||||
| return withBrowser(async (id) => { | ||||||||||||||||||||||||||||||||||||||
| // Element screenshots can't use fullPage; full-page shots can. | ||||||||||||||||||||||||||||||||||||||
| const target = p.selector ? `page.locator(${JSON.stringify(p.selector)})` : 'page'; | ||||||||||||||||||||||||||||||||||||||
| const shotOpts = p.selector ? `{ type: 'png' }` : `{ type: 'png', fullPage: true }`; | ||||||||||||||||||||||||||||||||||||||
| const code = [ | ||||||||||||||||||||||||||||||||||||||
| `await page.setViewportSize({ width: ${width}, height: ${height} });`, | ||||||||||||||||||||||||||||||||||||||
| `await page.goto(${JSON.stringify(p.url)}, { waitUntil: 'load', timeout: ${DEFAULT_TIMEOUT_MS} });`, | ||||||||||||||||||||||||||||||||||||||
| `const buf = await ${target}.screenshot(${shotOpts});`, | ||||||||||||||||||||||||||||||||||||||
| `return { screenshotBase64: buf.toString('base64'), width: ${width}, height: ${height} };`, | ||||||||||||||||||||||||||||||||||||||
| ].join('\n'); | ||||||||||||||||||||||||||||||||||||||
| return (await execute(id, code)).result as { | ||||||||||||||||||||||||||||||||||||||
| screenshotBase64: string; | ||||||||||||||||||||||||||||||||||||||
| width: number; | ||||||||||||||||||||||||||||||||||||||
| height: number; | ||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
| return result; | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| name: 'run_playwright', | ||||||||||||||||||||||||||||||||||||||
| description: 'Execute Playwright code in a Kernel.sh cloud browser. For complex browser automation like testing embed players or monitoring dashboards.', | ||||||||||||||||||||||||||||||||||||||
| description: | ||||||||||||||||||||||||||||||||||||||
| 'Execute Playwright code in a Kernel cloud browser. For complex browser automation like testing embed players or monitoring dashboards.', | ||||||||||||||||||||||||||||||||||||||
| parameters: { | ||||||||||||||||||||||||||||||||||||||
| code: { type: 'string', description: 'Playwright JavaScript code to execute', required: true }, | ||||||||||||||||||||||||||||||||||||||
| url: { type: 'string', description: 'Starting URL (optional)', required: false }, | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| schema: playwrightSchema, | ||||||||||||||||||||||||||||||||||||||
| handler: async (params: Record<string, unknown>) => { | ||||||||||||||||||||||||||||||||||||||
| const result = await kernelFetch<{ output: string; logs: string[] }>('/v1/browsers/execute', { | ||||||||||||||||||||||||||||||||||||||
| code: params.code, | ||||||||||||||||||||||||||||||||||||||
| url: params.url, | ||||||||||||||||||||||||||||||||||||||
| const p = playwrightSchema.parse(params); | ||||||||||||||||||||||||||||||||||||||
| return withBrowser(async (id) => { | ||||||||||||||||||||||||||||||||||||||
| if (p.url) { | ||||||||||||||||||||||||||||||||||||||
| await execute( | ||||||||||||||||||||||||||||||||||||||
| id, | ||||||||||||||||||||||||||||||||||||||
| `await page.goto(${JSON.stringify(p.url)}, { waitUntil: 'load', timeout: ${DEFAULT_TIMEOUT_MS} });`, | ||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| const res = await execute(id, p.code); | ||||||||||||||||||||||||||||||||||||||
| const logs = [res.stdout, res.stderr].filter( | ||||||||||||||||||||||||||||||||||||||
| (line): line is string => typeof line === 'string' && line.length > 0, | ||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||
| return { output: res.result ?? '', logs }; | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
| return result; | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Semgrep identified an issue in your code:
Insecure WebSocket Detected. WebSocket Secure (wss) should be used for all WebSocket connections.
To resolve this comment:
🔧 No guidance has been designated for this issue. Fix according to your organization's approved methods.
💬 Ignore this finding
Reply with Semgrep commands to ignore this finding.
/fp <comment>for false positive/ar <comment>for acceptable risk/other <comment>for all other reasonsAlternatively, triage in Semgrep AppSec Platform to ignore the finding created by detect-insecure-websocket.
You can view more details about this finding in the Semgrep AppSec Platform.