feat(runtime): let a paired desktop register its phones as phone devices (#26427)

* feat(runtime): let a paired desktop register its phones as delegated mobile devices

A desktop paired to this runtime can now call pairing.delegatedMobileDevice.sync to make each of
its phones an ordinary mobile-scope device it owns, so the phone mirrors that host through the
desktop. Children are never pending, inherit the parent's reach, and die with the parent's grant.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(runtime): move the mobile device revoke primitive below dispatch

Dispatch binds the caller-bound delegated-device context, so the revoke path it needs now lives in
a layer under it as a pure move; this drops the abstract method seam. No behaviour change.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(runtime): keep a runtime grant until its delegated phones are revoked

A failed child cleanup now leaves the parent in place so a retry can still find the children, and
revokeRuntimeAccess lives beside the revoke it cascades to. Adds restart, rename and bounds coverage,
shares credential minting, and drops the timing-dependent census.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(runtime): fail a delegated sync whose dropped phone cannot be revoked

Matches the parent revoke: a child whose cleanup could not be saved is kept and the sync errors
before minting anything, so the desktop retries instead of believing the server matches its list.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(runtime): refuse pairing admin to phones delegated through a paired desktop

A delegated child phone could call the mobile-allowlisted pairing-admin methods
(push registration, relay provisioning) that its parent desktop is never granted.
The caller scope now marks delegated phones and denies them that permission.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
This commit is contained in:
Jinwoo Hong
2026-10-11 02:54:09 -04:00
committed by GitHub
parent 7abee44e78
commit de00a4f03c
15 changed files with 673 additions and 117 deletions
@@ -0,0 +1,34 @@
import { z } from 'zod'
import type { RUNTIME_CAPABILITIES } from './protocol-version'
// Why: a paired desktop relays each of its phones to this host as an ordinary phone device it owns.
export const DELEGATED_MOBILE_DEVICE_SYNC_METHOD = 'pairing.delegatedMobileDevice.sync'
// Why: a desktop relays phones only to a host advertising this; `satisfies` ties it to RUNTIME_CAPABILITIES.
export const DELEGATED_MOBILE_DEVICES_RUNTIME_CAPABILITY =
'pairing.delegated-mobile-devices.v1' satisfies (typeof RUNTIME_CAPABILITIES)[number]
export const DELEGATED_MOBILE_DEVICE_SYNC_MAX_PHONES = 32
const DelegatedPhoneSchema = z
.object({
phoneKey: z.string().min(1).max(128),
name: z.string().min(1).max(128)
})
.strict()
export const DelegatedMobileDeviceSyncParamsSchema = z
.object({
phones: z
.array(DelegatedPhoneSchema)
.max(DELEGATED_MOBILE_DEVICE_SYNC_MAX_PHONES)
.refine((phones) => new Set(phones.map((phone) => phone.phoneKey)).size === phones.length, {
message: 'duplicate phoneKey'
})
})
.strict()
export type DelegatedPhone = z.infer<typeof DelegatedPhoneSchema>
export type DelegatedMobileDeviceSyncParams = z.infer<typeof DelegatedMobileDeviceSyncParamsSchema>
export type DelegatedMobileDeviceSyncResult = {
devices: { phoneKey: string; deviceId: string; token: string }[]
}
+1
View File
@@ -436,6 +436,7 @@ export const RUNTIME_CAPABILITIES = [
...SKILL_RUNTIME_CAPABILITIES,
...AUTOMATION_RUNTIME_CAPABILITIES,
NOTIFICATIONS_REMOTE_PUSH_RUNTIME_CAPABILITY,
'pairing.delegated-mobile-devices.v1',
PREFLIGHT_WORKSPACE_SCOPED_RUNTIME_CAPABILITY,
PREFLIGHT_OTHER_RUNTIME_REFUSAL_RUNTIME_CAPABILITY,
WORKSPACE_PORTS_HOST_SCOPED_RUNTIME_CAPABILITY,
+2
View File
@@ -18,6 +18,7 @@ import {
BrowserNetworkTunnelAttachParams
} from '../browser-client-host-protocol'
import { BrowserClientPageMetadataParams } from '../browser-client-page-metadata-protocol'
import { DelegatedMobileDeviceSyncParamsSchema } from '../delegated-mobile-device-contract'
import {
PairingGetEndpointsParamsSchema,
PairingProvisionRelayParamsSchema
@@ -1109,6 +1110,7 @@ export const RPC_PARAMS_BY_METHOD = {
'orchestration.workerStart': WorkerStartParams,
'orchestration.workerStop': WorkerDispatchParamsOfOrchestrationWorkerStopParams,
'orchestration.workerTerminalUserInput': OrchestrationWorkerTerminalUserInputParams,
'pairing.delegatedMobileDevice.sync': DelegatedMobileDeviceSyncParamsSchema,
'pairing.getEndpoints': PairingGetEndpointsParamsSchema,
'pairing.provisionRelay': PairingProvisionRelayParamsSchema,
'plugins.consent': pluginConsentRequestSchema,