Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/lib/browser-routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const BROWSER_ROUTING_SUBRESOURCES_ENV = 'KERNEL_BROWSER_ROUTING_SUBRESOURCES';
// Path prefixes eligible for direct-to-VM routing. "telemetry/stream" is the live
// SSE endpoint (served by the VM); "telemetry/events" is a historical read served
// by the control plane (S2) and must NOT be here.
const DEFAULT_BROWSER_ROUTING_SUBRESOURCES = ['curl', 'telemetry/stream'];
const DEFAULT_BROWSER_ROUTING_SUBRESOURCES = ['curl', 'telemetry/stream', 'computer', 'playwright'];
const BROWSER_ROUTE_CACHEABLE_PATH = /^\/(?:v\d+\/)?browsers(?:\/[^/]+)?\/?$/;
const BROWSER_POOL_ACQUIRE_PATH = /^\/(?:v\d+\/)?browser_pools\/[^/]+\/acquire\/?$/;
const BROWSER_DELETE_BY_ID_PATH = /^\/(?:v\d+\/)?browsers\/([^/]+)\/?$/;
Expand Down
98 changes: 95 additions & 3 deletions tests/lib/browser-routing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -444,20 +444,28 @@ describe('browser routing', () => {
).rejects.toThrow(/unsupported HTTP method/i);
});

test('defaults browser routing subresources to curl and telemetry/stream when env is unset', async () => {
test('defaults browser routing subresources when env is unset', async () => {
await withBrowserRoutingEnv(undefined, async () => {
expect(browserRoutingSubresourcesFromEnv()).toEqual(['curl', 'telemetry/stream']);
expect(browserRoutingSubresourcesFromEnv()).toEqual([
'curl',
'telemetry/stream',
'computer',
'playwright',
]);
});
});

test('allowlist matching is segment-boundary aware (telemetry/events stays on the control plane)', () => {
const prefixes = ['curl', 'telemetry/stream'];
const prefixes = ['curl', 'telemetry/stream', 'computer', 'playwright'];
expect(matchesDirectVMPrefix('telemetry/stream', prefixes)).toBe(true);
expect(matchesDirectVMPrefix('telemetry/stream/x', prefixes)).toBe(true);
expect(matchesDirectVMPrefix('telemetry/events', prefixes)).toBe(false);
expect(matchesDirectVMPrefix('telemetry/streaming-config', prefixes)).toBe(false);
expect(matchesDirectVMPrefix('telemetry', prefixes)).toBe(false);
expect(matchesDirectVMPrefix('curl/raw', prefixes)).toBe(true);
expect(matchesDirectVMPrefix('computer/screenshot', prefixes)).toBe(true);
expect(matchesDirectVMPrefix('playwright/execute', prefixes)).toBe(true);
expect(matchesDirectVMPrefix('process/exec', prefixes)).toBe(false);
expect(matchesDirectVMPrefix('fs/read', prefixes)).toBe(false);
});

Expand Down Expand Up @@ -498,4 +506,88 @@ describe('browser routing', () => {
expect(browserRoutingSubresourcesFromEnv()).toEqual([]);
});
});

test('routes computer screenshot and playwright execute to the VM by default', async () => {
await withBrowserRoutingEnv(undefined, async () => {
const calls: Array<{ url: string; headers: Headers }> = [];
const kernel = new Kernel({
apiKey: 'k',
baseURL: 'https://api.example/',
fetch: async (input, init?: RequestInit) => {
const url = normalizeURL(input);
const headers = input instanceof Request ? new Headers(input.headers) : new Headers(init?.headers);
calls.push({ url, headers });
if (url === 'https://api.example/browsers') {
return Response.json({
session_id: 'sess-1',
base_url: 'http://browser-session.test/browser/kernel',
cdp_ws_url: 'wss://browser-session.test/browser/cdp?jwt=token-abc',
});
}
if (url.includes('/computer/screenshot')) {
return new Response(new Uint8Array([1, 2, 3]), {
status: 200,
headers: { 'content-type': 'image/png' },
});
}
return Response.json({ success: true });
},
});

await kernel.browsers.create();
await kernel.browsers.computer.captureScreenshot('sess-1');
await kernel.browsers.playwright.execute('sess-1', { code: 'return 1' });

expect(calls[1]?.url).toBe(
'http://browser-session.test/browser/kernel/computer/screenshot?jwt=token-abc',
);
expect(calls[1]?.headers.get('authorization')).toBeNull();
expect(calls[2]?.url).toBe(
'http://browser-session.test/browser/kernel/playwright/execute?jwt=token-abc',
);
expect(calls[2]?.headers.get('authorization')).toBeNull();
});
});

test('keeps process, fs, and telemetry/events on the API origin by default', async () => {
await withBrowserRoutingEnv(undefined, async () => {
const calls: string[] = [];
const kernel = new Kernel({
apiKey: 'k',
baseURL: 'https://api.example/',
fetch: async (input) => {
const url = normalizeURL(input);
calls.push(url);
if (url === 'https://api.example/browsers') {
return Response.json({
session_id: 'sess-1',
base_url: 'http://browser-session.test/browser/kernel',
cdp_ws_url: 'wss://browser-session.test/browser/cdp?jwt=token-abc',
});
}
if (url.includes('/telemetry/events')) {
return Response.json([]);
}
if (url.includes('/fs/read_file')) {
return new Response(new Uint8Array([1]), {
status: 200,
headers: { 'content-type': 'application/octet-stream' },
});
}
return Response.json({ exit_code: 0, stdout_b64: '', stderr_b64: '' });
},
});

await kernel.browsers.create();
await kernel.browsers.process.exec('sess-1', { command: 'echo' });
await kernel.browsers.fs.readFile('sess-1', { path: '/tmp/x' });
await kernel.browsers.telemetry.events('sess-1');

expect(calls.slice(1)).toEqual([
'https://api.example/browsers/sess-1/process/exec',
'https://api.example/browsers/sess-1/fs/read_file?path=%2Ftmp%2Fx',
'https://api.example/browsers/sess-1/telemetry/events',
]);
});
});
});
Loading