Skip to content
Merged
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
13 changes: 9 additions & 4 deletions src/main/runtime/browser/browser-client-page-opening.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,17 +125,22 @@ export function openBrowserUrlOnClient(
host: BrowserClientPageOpeningHost,
params: { url: string; worktree: string }
): Promise<{ browserPageId: string }> {
const protocol = new URL(params.url).protocol
if (protocol !== 'http:' && protocol !== 'https:') {
throw new BrowserError('invalid_argument', 'Only http(s) URLs can be opened on the client.')
}
assertClientOpenableUrl(params.url)
return createBrowserPageOnPairedHost(
host,
{ url: params.url, worktree: params.worktree, activate: true, navigation: 'caller' },
DEFAULT_BROWSER_PROFILE_ID
)
}

/** `orca open-url` opens only web pages, wherever the page ends up rendering. */
export function assertClientOpenableUrl(url: string): void {
const protocol = new URL(url).protocol
if (protocol !== 'http:' && protocol !== 'https:') {
throw new BrowserError('invalid_argument', 'Only http(s) URLs can be opened on the client.')
}
}

/** A page its caller did not place goes to the one paired desktop hosting this runtime's pages. */
export async function createBrowserPageOnPairedHost(
host: BrowserClientPageOpeningHost,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { RpcDispatcher } from '../rpc/dispatcher'
import { DESKTOP_BROWSER_BRIDGE_RPC_METHODS } from '../rpc/methods/desktop-browser-bridge'
import { BrowserTarget } from '../../../shared/rpc-contract/rpc-param-primitives'
import { Goto } from '../../../shared/rpc-contract/browser-params'
import { BrowserOpenUrlParams } from '../../../shared/rpc-contract/browser-tab-create-params'
import {
getDesktopBrowserBridgeRegistry,
setDesktopBrowserBridgeHostEnabled
Expand All @@ -23,6 +24,7 @@ const ForwardedEvent = z.object({ requestId: z.string() }).passthrough()

const localGoto = vi.fn(async () => ({ url: 'local', title: '' }))
const localCookie = vi.fn(async () => ({ cookies: [] }))
const localOpenUrl = vi.fn(async () => ({ browserPageId: 'paired-host-page' }))

const METHODS = [
...DESKTOP_BROWSER_BRIDGE_RPC_METHODS,
Expand All @@ -37,6 +39,12 @@ const METHODS = [
permission: 'workspace',
params: BrowserTarget,
handler: () => localCookie()
}),
defineMethod({
name: 'browser.openUrl',
permission: 'workspace',
params: BrowserOpenUrlParams,
handler: () => localOpenUrl()
})
]

Expand Down Expand Up @@ -108,6 +116,7 @@ afterEach(() => {
setDesktopBrowserBridgeHostEnabled(false)
localGoto.mockClear()
localCookie.mockClear()
localOpenUrl.mockClear()
})

describe('desktop browser bridge on orcad', () => {
Expand Down Expand Up @@ -171,6 +180,98 @@ describe('desktop browser bridge on orcad', () => {
expect(localGoto).not.toHaveBeenCalled()
})

it('sends open-url to the attached desktop as the tab create it already runs', async () => {
setDesktopBrowserBridgeHostEnabled(true)
const { runtime } = orcadRuntime()
const dispatcher = new RpcDispatcher({ runtime, methods: METHODS })
const { events } = await attachDesktop(dispatcher)
const ready = ReadyEvent.parse(events[0])

const call = cliCall(dispatcher, 'browser.openUrl', {
url: 'https://example.com/',
worktree: 'path:/srv/app'
})
await vi.waitFor(() => expect(events).toHaveLength(2))
const forwarded = ForwardedEvent.parse(events[1])
// An older bridged desktop has always accepted browser.tabCreate; it never knew browser.openUrl.
expect(forwarded).toEqual({
type: 'request',
requestId: forwarded.requestId,
method: 'browser.tabCreate',
timeoutMs: expect.any(Number),
workspaceId: 'repo-1::/srv/app',
params: { url: 'https://example.com/', worktree: 'id:repo-1::/srv/app' }
})
await dispatcher.dispatch(
{
id: 'answer',
authToken: 'bound',
method: 'desktopBrowserBridge.result',
params: {
attachmentId: ready.attachmentId,
requestId: forwarded.requestId,
ok: true,
result: { browserPageId: 'desktop-page' }
}
},
DESKTOP
)

await expect(call).resolves.toMatchObject({
ok: true,
result: { browserPageId: 'desktop-page' }
})
expect(localOpenUrl).not.toHaveBeenCalled()
})

it('refuses a non-web open-url on the host before it reaches the desktop', async () => {
setDesktopBrowserBridgeHostEnabled(true)
const { runtime } = orcadRuntime()
const dispatcher = new RpcDispatcher({ runtime, methods: METHODS })
const { events } = await attachDesktop(dispatcher)

const response = await cliCall(dispatcher, 'browser.openUrl', {
url: 'file:///etc/hosts',
worktree: 'path:/srv/app'
})

expect(response).toMatchObject({ ok: false, error: { code: 'invalid_argument' } })
expect(events).toHaveLength(1)
})

it('names the setting for open-url when no desktop hosts or shares a browser', async () => {
setDesktopBrowserBridgeHostEnabled(true)
const { runtime } = orcadRuntime()
const dispatcher = new RpcDispatcher({ runtime, methods: METHODS })

const response = await cliCall(dispatcher, 'browser.openUrl', {
url: 'https://example.com/',
worktree: 'path:/srv/app'
})

expect(response).toMatchObject({
ok: false,
error: { code: 'desktop_browser_bridge_unavailable' }
})
expect(response.ok ? '' : response.error.message).toContain(
DESKTOP_BROWSER_BRIDGE_SETTING_LABEL
)
expect(localOpenUrl).not.toHaveBeenCalled()
})

it("leaves open-url to a desktop hosting this runtime's pages while none shares its browser", async () => {
setDesktopBrowserBridgeHostEnabled(true)
const { runtime } = orcadRuntime({ localBrowser: true })
const dispatcher = new RpcDispatcher({ runtime, methods: METHODS })

await expect(
cliCall(dispatcher, 'browser.openUrl', {
url: 'https://example.com/',
worktree: 'path:/srv/app'
})
).resolves.toMatchObject({ ok: true, result: { browserPageId: 'paired-host-page' } })
})

it('never forwards a command outside the allowlist', async () => {
setDesktopBrowserBridgeHostEnabled(true)
const { runtime } = orcadRuntime()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
isDesktopBrowserBridgeMethod
} from '../../../shared/desktop-browser-bridge-protocol'
import type { RpcCallerIdentity } from '../rpc/rpc-caller-identity'
import { assertClientOpenableUrl } from '../browser/browser-client-page-opening'
import type { DesktopBrowserBridgeRegistry } from './desktop-browser-bridge-registry'

export type DesktopBrowserBridgeRoute = { handled: false } | { handled: true; result: unknown }
Expand Down Expand Up @@ -38,7 +39,10 @@ export async function routeDesktopBrowserBridgeRpc(options: {
) {
return { handled: false }
}
const forwarded = isDesktopBrowserBridgeMethod(method)
// Why tabCreate: open-url is `tab create --url` to the desktop, so it adds no new desktop surface
// and an older bridged desktop already runs it under the same scope, URL and focus rules.
const forwardedMethod = method === 'browser.openUrl' ? 'browser.tabCreate' : method
const forwarded = isDesktopBrowserBridgeMethod(forwardedMethod)
if (!options.registry.hasAttachment()) {
if (options.localBrowserAvailable || !forwarded) {
return { handled: false }
Expand All @@ -58,6 +62,9 @@ export async function routeDesktopBrowserBridgeRpc(options: {
)
}
const params: Record<string, unknown> = isRecord(options.params) ? { ...options.params } : {}
if (method === 'browser.openUrl') {
assertClientOpenableUrl(String(params.url))
}
let workspaceId: string | undefined
// Why resolve here: only orcad knows this host's selectors (path:, branch:, active).
if (typeof params.worktree === 'string' && params.worktree.length > 0) {
Expand All @@ -67,8 +74,8 @@ export async function routeDesktopBrowserBridgeRpc(options: {
delete params.worktree
}
const result = await options.registry.forward(
{ method, params, ...(workspaceId ? { workspaceId } : {}) },
{ timeoutMs: forwardTimeoutMs(method, params), signal: options.signal }
{ method: forwardedMethod, params, ...(workspaceId ? { workspaceId } : {}) },
{ timeoutMs: forwardTimeoutMs(forwardedMethod, params), signal: options.signal }
)
return { handled: true, result }
}
Expand Down
3 changes: 2 additions & 1 deletion tests/e2e/ssh-orcad-browser-capabilities.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ test('a backendless managed host offers desktop-hosted pages and refuses its own
const refused = await call('browser.tabList', {})
console.log('[managed-browser-command]', JSON.stringify(refused))
expect(refused).toMatchObject({ ok: false, error: { code: 'browser_unavailable' } })
// A managed SSH desktop keeps its SSH-routed tabs, so it never takes this host's pages.
// A paired caller's open-url asks for a desktop hosting this host's pages, which a managed SSH
// desktop never is; the host's own CLI reaches it through the browser bridge instead.
const openUrl = await call('browser.openUrl', {
url: 'https://example.org/',
worktree: 'active'
Expand Down
24 changes: 23 additions & 1 deletion tests/e2e/ssh-orcad-desktop-browser-bridge.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
*
* Off, the CLI is refused with an error that names the setting. On, `orca tab create` opens a
* desktop tab in the host's workspace (its traffic leaves through the host, which alone resolves
* the fixture origin) and `orca goto` navigates it. Off again, the host is refused at once.
* the fixture origin), `orca goto` navigates it and `orca open-url` opens one the same way. Off
* again, the host is refused at once.
*/
import type { ElectronApplication, Page } from '@stablyai/playwright-test'
import { z } from 'zod'
Expand Down Expand Up @@ -185,6 +186,23 @@ test('a managed SSH host drives the desktop browser by default until its CLI con
const listed = await hostCli(page, target, tabId, 'orca tab list')
expect(listed.ok).toBe(true)
expect(JSON.stringify(listed.result)).toContain(browserPageId)
// `orca open-url` reaches the desktop the same way, as a tab in the host's workspace.
const openedUrl = `${SSH_REMOTE_ONLY_ORIGIN}/echo/bridge-open-url`
const opened = await hostCliUntilOk(page, target, tabId, `orca open-url --url ${openedUrl}`)
const openedPageId = String(opened.result?.browserPageId ?? '')
expect(openedPageId).not.toBe('')
await expect
.poll(() => browserTabUrls(page, seeded.worktreeId), { timeout: 30_000 })
.toContain(openedUrl)
await expect
.poll(() => readSshRemoteOnlyRequests(target).map((request) => request.path), {
timeout: 60_000,
message: 'the desktop tab open-url opened never loaded through the host'
})
.toContain('/echo/bridge-open-url')
expect((await hostCli(page, target, tabId, `orca tab close --page ${openedPageId}`)).ok).toBe(
true
)
await page.screenshot({ path: testInfo.outputPath('desktop-browser-bridge-on.png') })
// A desktop file is never reachable, even through a tab the host may drive.
const fileRead = await hostCli(
Expand All @@ -210,6 +228,10 @@ test('a managed SSH host drives the desktop browser by default until its CLI con
.toBe('desktop_browser_bridge_unavailable')
const refused = await hostCli(page, target, tabId, 'orca tab list')
expect(refused.error?.message).toContain(DESKTOP_BROWSER_BRIDGE_SETTING_LABEL)
// open-url names the same setting, not a desktop that is "offline".
const openRefused = await hostCli(page, target, tabId, `orca open-url --url ${openedUrl}`)
expect(openRefused.error?.code).toBe('desktop_browser_bridge_unavailable')
expect(openRefused.error?.message).toContain(DESKTOP_BROWSER_BRIDGE_SETTING_LABEL)
} finally {
try {
if (app) {
Expand Down
Loading