mirror of
https://github.com/stablyai/orca.git
synced 2026-11-02 01:57:57 +00:00
fix(mobile): run targeted desktop-owned calls on the desktop
The phone tags every call on a workspace screen with its execution host, so a paired-desktop method carrying `runtime:<env>` now runs locally instead of failing 'forbidden'. The Mac stays the only reader of the routing table. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
This commit is contained in:
@@ -422,12 +422,30 @@ describe('mobile desktop relay: phone -> desktop -> server', () => {
|
||||
expect(isOk(await phone.next('after-sync'))).toBe(true)
|
||||
})
|
||||
|
||||
it('refuses desktop-owned methods and unknown servers without running anything on the desktop', async () => {
|
||||
it('answers a targeted desktop-owned call on the desktop, exactly as the untargeted one', async () => {
|
||||
const { desktop } = await startTopology()
|
||||
const phone = await connectPhone(desktop.server, 'iPhone')
|
||||
const opensBefore = passthroughOpens.count
|
||||
|
||||
for (const method of ['settings.get', 'status.get']) {
|
||||
phone.send(`${method}:targeted`, method, {})
|
||||
phone.send(`${method}:untargeted`, method, {}, null)
|
||||
const targeted = await phone.next(`${method}:targeted`)
|
||||
const untargeted = await phone.next(`${method}:untargeted`)
|
||||
expect(isOk(targeted)).toBe(true)
|
||||
expect(targeted.result).toEqual(untargeted.result)
|
||||
}
|
||||
expect(passthroughOpens.count).toBe(opensBefore)
|
||||
// An execution-host method under the same target still relays.
|
||||
phone.send('list', 'terminal.list', {})
|
||||
expect(isOk(await phone.next('list'))).toBe(true)
|
||||
expect(passthroughOpens.count).toBe(opensBefore + 1)
|
||||
})
|
||||
|
||||
it('refuses binary-frame methods and unknown servers without running anything on the desktop', async () => {
|
||||
const { desktop, desktopSpawns } = await startTopology()
|
||||
const phone = await connectPhone(desktop.server, 'iPhone')
|
||||
|
||||
phone.send('settings', 'settings.get', {})
|
||||
expect(errorCode(await phone.next('settings'))).toBe('forbidden')
|
||||
phone.send('multiplex', 'terminal.multiplex', {})
|
||||
expect(errorCode(await phone.next('multiplex'))).toBe('forbidden')
|
||||
const create = {
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
openRemoteRuntimePassthroughSocket,
|
||||
type RemoteRuntimePassthroughSocket
|
||||
} from '../../../shared/remote-runtime-passthrough-socket'
|
||||
import { MOBILE_RPC_METHOD_ROUTES } from '../runtime-rpc/runtime-rpc-mobile-method-routing'
|
||||
import { syncDelegatedPhoneGrants, type DelegatedPhoneGrants } from './delegated-phone-grants'
|
||||
import type { MobileDesktopRelayHost, MobileDesktopRelayHosts } from './mobile-desktop-relay-hosts'
|
||||
import { RELAYED_STREAM_CARRIERS } from './relayed-stream-carriers'
|
||||
@@ -53,7 +52,7 @@ class RelayLink {
|
||||
* `pairing.delegatedMobileDevice.sync`, tokens held in memory only). Contract:
|
||||
* - The desktop never interprets replies; it renumbers only host stream ids (RELAYED_STREAM_CARRIERS).
|
||||
* - Upstream, the phone's desktop token is swapped wherever it appears, not per field.
|
||||
* - Only `execution-host` methods relay (MOBILE_RPC_METHOD_ROUTES); both tables have census tests.
|
||||
* - Only `execution-host` methods (MOBILE_RPC_METHOD_ROUTES) reach here; `paired-desktop` ones run locally.
|
||||
* - A server without the delegated-devices capability is `update-needed`; nothing relays to it.
|
||||
* - A server-side revoke of a phone is final until the next sync.
|
||||
* - Hidden until S2: MOBILE_DESKTOP_RELAY_RUNTIME_CAPABILITY is not advertised yet.
|
||||
@@ -89,12 +88,6 @@ export class MobileDesktopRelay {
|
||||
request: { id: string; method: string },
|
||||
frame: string
|
||||
): void {
|
||||
if (MOBILE_RPC_METHOD_ROUTES.get(request.method) !== 'execution-host') {
|
||||
phone.reply(
|
||||
failure(request.id, 'forbidden', `Method '${request.method}' runs on the paired desktop`)
|
||||
)
|
||||
return
|
||||
}
|
||||
if (RELAYED_STREAM_CARRIERS.get(request.method) === 'phone-binary-frames') {
|
||||
phone.reply(
|
||||
failure(
|
||||
|
||||
@@ -11,6 +11,7 @@ import type { DeviceScope } from '../device-registry'
|
||||
import { RuntimeRpcMobileDevices } from './runtime-rpc-mobile-devices'
|
||||
import { classifyRuntimeLongPoll } from './runtime-rpc-long-poll'
|
||||
import { MOBILE_RPC_METHOD_ALLOWLIST } from './runtime-rpc-mobile-method-allowlist'
|
||||
import { MOBILE_RPC_METHOD_ROUTES } from './runtime-rpc-mobile-method-routing'
|
||||
import { parseExecutionHostId } from '../../../shared/execution-host'
|
||||
|
||||
// Why: status.get has no per-connection context in the dispatcher, so stamp the scope here at the transport boundary.
|
||||
@@ -45,6 +46,10 @@ export class RuntimeRpcWebSocketDispatch extends RuntimeRpcMobileDevices {
|
||||
if (target?.kind === 'local' || target?.kind === 'ssh') {
|
||||
return false
|
||||
}
|
||||
// Why: the phone tags every call on a workspace screen; only this Mac's routing table decides.
|
||||
if (target && MOBILE_RPC_METHOD_ROUTES.get(request.method) === 'paired-desktop') {
|
||||
return false
|
||||
}
|
||||
if (!target) {
|
||||
reply(
|
||||
JSON.stringify(this.buildError(request.id, 'invalid_argument', 'Invalid executionHost'))
|
||||
|
||||
Reference in New Issue
Block a user