refactor(mobile): delete the shell browser translators, streams and grants

The browser capability is gone from the bridge registry, so the shell no longer
names a browser RPC, sanitizes a tab URL or chunks a frame. hostRequest carries
pointer traffic now, so its bucket takes over the retired pointer grant's rate.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
This commit is contained in:
Jinwoo-H
2026-09-07 15:07:47 -04:00
parent f6d32ea7cf
commit 4bb3f2e6e6
11 changed files with 7 additions and 137 deletions
@@ -244,7 +244,6 @@ export class MobileWebCapabilityBroker {
agentHistoryPager: this.authorities.agentHistoryPager,
agentHistoryResume: this.authorities.agentHistoryResume,
accountSubscriptions: this.subscriptions.account,
browserStreams: this.subscriptions.browser,
sourceControlBranchCompare: this.authorities.sourceControlBranchCompare,
speechAuthority: this.speechAuthority,
workspaceSubscriptions: this.subscriptions.workspace,
@@ -49,7 +49,7 @@ describe('mobile web capability dispatch census', () => {
})
expect(unresolved.map(({ capability, operation }) => `${capability}.${operation}`)).toEqual([])
expect(registeredOperations()).toHaveLength(199)
expect(registeredOperations()).toHaveLength(191)
})
it('carries a dispatch arm for exactly the capabilities that own operations of that mode', () => {
@@ -1,4 +1,3 @@
import { MobileWebBrowserStreamPayloadSchema } from '../../../src/shared/mobile-web/browser-operation-contract'
import { MobileWebWorkspaceSubscribePayloadSchema } from '../../../src/shared/mobile-web/bridge-operation-contract'
import type { MobileWebBridgePageMessage } from '../../../src/shared/mobile-web/bridge-contract'
import type { MobileWebBridgeCapability } from '../../../src/shared/mobile-web/bridge-operation-registry'
@@ -7,7 +6,6 @@ import { executeWorkspace } from './mobile-web-workspace-capability'
import { executeMobileWebAccountCapability } from './mobile-web-account-capability'
import { executeMobileWebAgentHistoryOperation } from './mobile-web-agent-history-operations'
import { MobileWebBrokerError } from './mobile-web-broker-error'
import { executeMobileWebBrowserOperation } from './mobile-web-browser-operations'
import type { MobileWebCapabilityExecutionDependencies } from './mobile-web-capability-execution-dependencies'
import { executeMobileWebFileOperation } from './mobile-web-file-operations'
import { executeMobileWebMarkdownOperation } from './mobile-web-markdown-operations'
@@ -53,15 +51,6 @@ async function executeNavigation(args: Deps, request: OnceRequest): Promise<unkn
})
}
async function executeBrowser(args: Deps, request: OnceRequest): Promise<unknown> {
return executeMobileWebBrowserOperation({
operation: request.operation,
payload: request.payload,
client: args.connectedClient(),
workspaceAuthority: args.workspaceAuthority
})
}
async function executeTerminal(args: Deps, request: OnceRequest): Promise<unknown> {
return args.terminalStreams.handle(request.payload, args.connectedClient())
}
@@ -158,7 +147,6 @@ export const MOBILE_WEB_ONCE_CAPABILITY_ARMS: Partial<Record<MobileWebBridgeCapa
navigation: executeNavigation,
agentHistory: (args) => executeMobileWebAgentHistoryOperation(args),
account: (args) => executeMobileWebAccountCapability(args),
browser: executeBrowser,
workspace: executeWorkspace,
settings: executeWorkspace,
terminal: executeTerminal,
@@ -169,18 +157,6 @@ export const MOBILE_WEB_ONCE_CAPABILITY_ARMS: Partial<Record<MobileWebBridgeCapa
task: executeTask
}
async function subscribeBrowser(args: Deps, request: SubscriptionRequest): Promise<unknown> {
requireSubscribeOperation(request)
MobileWebBrowserStreamPayloadSchema.parse(request.payload)
args.browserStreams.start({
requestId: request.requestId,
subscriptionId: request.subscriptionId,
payload: request.payload,
client: args.connectedClient()
})
return null
}
async function subscribeWorkspace(args: Deps, request: SubscriptionRequest): Promise<unknown> {
if (request.operation === 'hostSubscribe') {
args.hostSubscriptions.start({
@@ -228,7 +204,6 @@ export const MOBILE_WEB_SUBSCRIPTION_CAPABILITY_ARMS: Partial<
Record<MobileWebBridgeCapability, SubscriptionArm>
> = {
account: (args) => executeMobileWebAccountCapability(args),
browser: subscribeBrowser,
workspace: subscribeWorkspace,
terminal: subscribeTerminal,
speech: subscribeSpeech
@@ -5,7 +5,6 @@ import type { MobileWebAccountSubscriptions } from './mobile-web-account-subscri
import type { MobileWebAgentHistoryAuthority } from './mobile-web-agent-history-authority'
import type { MobileWebAgentHistoryPager } from './mobile-web-agent-history-pager'
import type { MobileWebAgentHistoryResume } from './mobile-web-agent-history-resume'
import type { MobileWebBrowserStreams } from './mobile-web-browser-streams'
import type { MobileWebCommitMessageGeneration } from './mobile-web-commit-message-generation'
import type { MobileWebNavigationAuthority } from './mobile-web-navigation-operations'
import type { MobileWebNativeCapabilityAuthority } from './mobile-web-native-capability-authority'
@@ -33,7 +32,6 @@ export type MobileWebCapabilityExecutionDependencies = {
agentHistoryResume: MobileWebAgentHistoryResume
hostSubscriptions: MobileWebHostSubscriptions
accountSubscriptions: MobileWebAccountSubscriptions
browserStreams: MobileWebBrowserStreams
sourceControlBranchCompare: MobileWebSourceControlBranchComparePager
speechAuthority: MobileWebSpeechAuthority
workspaceSubscriptions: MobileWebWorkspaceSubscriptions
@@ -5,14 +5,12 @@ import type {
MobileWebSubscriptionLedgerConfig,
MobileWebSubscriptionLedgerHandle
} from './mobile-web-subscription-ledger'
import { MobileWebBrowserStreams } from './mobile-web-browser-streams'
import type { MobileWebWorkspaceAuthority } from './mobile-web-workspace-authority'
import { MobileWebWorkspaceSubscriptions } from './mobile-web-workspace-subscriptions'
export class MobileWebCapabilitySubscriptions {
readonly host: MobileWebHostSubscriptions
readonly account: MobileWebAccountSubscriptions
readonly browser: MobileWebBrowserStreams
readonly workspace: MobileWebWorkspaceSubscriptions
private readonly ledgers: MobileWebSubscriptionLedgerHandle[]
@@ -31,12 +29,8 @@ export class MobileWebCapabilitySubscriptions {
workspaceAuthority: args.workspaceAuthority
})
this.account = new MobileWebAccountSubscriptions(shared)
this.browser = new MobileWebBrowserStreams({
...shared,
workspaceAuthority: args.workspaceAuthority
})
this.workspace = new MobileWebWorkspaceSubscriptions(shared)
this.ledgers = [this.host, this.account, this.browser, this.workspace]
this.ledgers = [this.host, this.account, this.workspace]
}
countForOperation(operationKey: string): number {
@@ -43,13 +43,6 @@ const REAUTHORIZATION_SITES: Record<string, number> = {
// Device-only mutations and handles consumed in one awaited call have no reauthorization window.
const NO_REAUTHORIZATION_WINDOW: readonly string[] = [
'workspace.activate',
'browser.back',
'browser.dialog',
'browser.forward',
'browser.keyboard',
'browser.navigate',
'browser.pointer',
'browser.reload',
'file.releaseTerminalArtifact',
'native.alert',
'native.clipboardWrite',
@@ -167,7 +160,7 @@ describe('mobile web mutation reauthorization census', () => {
}
expect(unaccounted).toEqual([])
expect(mutations()).toHaveLength(113)
expect(mutations()).toHaveLength(106)
})
it('exempts only registered mutations', () => {
@@ -185,6 +178,6 @@ describe('mobile web mutation reauthorization census', () => {
const capabilities = Object.keys(MOBILE_WEB_BRIDGE_OPERATIONS) as MobileWebBridgeCapability[]
expect([...kinds].sort()).toEqual(['mutation', 'read', 'subscription'])
expect(capabilities).toHaveLength(14)
expect(capabilities).toHaveLength(13)
})
})
@@ -1,4 +1,3 @@
import { MOBILE_WEB_PRODUCTION_BROWSER_GRANTS } from './mobile-web-production-browser-grants'
import { MOBILE_WEB_PRODUCTION_FILE_GRANTS } from './mobile-web-production-file-grants'
import { capabilityGrants, grantLimits, indexGrants } from './mobile-web-production-grant-table'
import { MOBILE_WEB_PRODUCTION_NAVIGATION_GRANTS } from './mobile-web-production-navigation-grants'
@@ -16,7 +15,7 @@ export type { MobileWebOperationGrant } from './mobile-web-production-grant-tabl
export const MOBILE_WEB_PRODUCTION_GRANTS = [
...capabilityGrants('workspace', {
hostSubscribe: grantLimits(600 * 1024, 1024, 8, 8, 2),
hostRequest: grantLimits(600 * 1024, 600 * 1024, 16, 32, 4),
hostRequest: grantLimits(600 * 1024, 600 * 1024, 16, 48, 24),
snapshot: grantLimits(1 * 1024, 128 * 1024, 2, 4, 1),
repositories: grantLimits(256, 128 * 1024, 2, 4, 1),
subscribe: grantLimits(256, 1 * 1024, 1, 4, 1),
@@ -40,7 +39,6 @@ export const MOBILE_WEB_PRODUCTION_GRANTS = [
}),
...MOBILE_WEB_PRODUCTION_SESSION_GRANTS,
...MOBILE_WEB_PRODUCTION_TERMINAL_GRANTS,
...MOBILE_WEB_PRODUCTION_BROWSER_GRANTS,
...MOBILE_WEB_PRODUCTION_FILE_GRANTS,
...MOBILE_WEB_PRODUCTION_SOURCE_CONTROL_GRANTS,
...MOBILE_WEB_PRODUCTION_SPEECH_GRANTS,
@@ -18,7 +18,6 @@ export function mobileWebRequestExpectsSubscription(request: {
request.capability === 'session' ||
request.capability === 'sourceControl' ||
request.capability === 'terminal' ||
request.capability === 'browser' ||
request.capability === 'nativeChat' ||
request.capability === 'speech') &&
request.operation === 'subscribe')
@@ -2,11 +2,9 @@ import { describe, expect, it, vi } from 'vitest'
import type { MobileWebSubscriptionClosure } from './mobile-web-subscription-closure'
import type { RpcClient } from '../transport/rpc-client'
import { MobileWebAccountSubscriptions } from './mobile-web-account-subscriptions'
import { MobileWebBrowserStreams } from './mobile-web-browser-streams'
import { MobileWebSpeechSubscriptions } from './mobile-web-speech-subscriptions'
import type { MobileWebSpeechEvent } from '../../../src/shared/mobile-web/speech-operation-contract'
import { MobileWebWorkspaceSubscriptions } from './mobile-web-workspace-subscriptions'
import { MobileWebWorkspaceAuthority } from './mobile-web-workspace-authority'
const SUBSCRIPTION_ID = 'subscription-1'
@@ -18,17 +16,13 @@ type Posts = {
type LedgerCase = {
name: string
// Browser drops an unparseable frame instead of retiring, so it has no invalid-message closure.
// A push-driven ledger has no host frame to reject, so it has no invalid-message closure.
invalidCode: 'invalid_message' | null
invalid: unknown
valid: unknown
open: (posts: Posts) => Promise<(value: unknown) => void>
}
function randomBytes(length: number): Uint8Array {
return new Uint8Array(length).fill(4)
}
function hostClient(): { client: RpcClient; emit: (value: unknown) => void } {
let listener: ((value: unknown) => void) | undefined
const client = {
@@ -54,12 +48,6 @@ function hostClient(): { client: RpcClient; emit: (value: unknown) => void } {
return { client, emit: (value) => listener?.(value) }
}
function pageWorkspace(): { authority: MobileWebWorkspaceAuthority; pageWorkspaceId: string } {
const authority = new MobileWebWorkspaceAuthority(randomBytes)
authority.synchronize([{ workspaceId: 'workspace-1', repoId: 'repo-1' }])
return { authority, pageWorkspaceId: authority.pageWorkspaceId('workspace-1') }
}
const LEDGER_CASES: LedgerCase[] = [
{
name: 'account',
@@ -91,36 +79,6 @@ const LEDGER_CASES: LedgerCase[] = [
return host.emit
}
},
{
name: 'browser',
invalidCode: null,
invalid: { type: 'bogus' },
valid: {
type: 'ready',
browserPageId: 'raw-page',
tab: { url: 'https://example.com', title: 'Example', canGoBack: false, canGoForward: false }
},
open: async (posts) => {
const host = hostClient()
const { authority, pageWorkspaceId } = pageWorkspace()
new MobileWebBrowserStreams({ ...posts, workspaceAuthority: authority }).start({
requestId: 'request-1',
subscriptionId: SUBSCRIPTION_ID,
payload: {
workspaceId: pageWorkspaceId,
pageId: 'raw-page',
format: 'jpeg',
quality: 72,
maxWidth: 800,
maxHeight: 600,
everyNthFrame: 1,
minFrameIntervalMs: 100
},
client: host.client
})
return host.emit
}
},
{
name: 'speech',
// Push-driven from the shell's dictation runtime, so no host frame can be unusable.
@@ -135,7 +93,7 @@ const LEDGER_CASES: LedgerCase[] = [
}
]
// Ledgers that retire on an unusable host message; browser drops the frame instead, so it is absent.
// Ledgers that retire on an unusable host message; a push-driven ledger has none, so it is absent.
const RETIRING_LEDGER_CASES = LEDGER_CASES.filter(
(ledger): ledger is LedgerCase & { invalidCode: 'invalid_message' } => ledger.invalidCode !== null
)
@@ -6,7 +6,6 @@ import type {
} from '../../../src/shared/mobile-web/bridge-contract'
import type { RpcClient } from '../transport/rpc-client'
import { MobileWebAccountSubscriptions } from './mobile-web-account-subscriptions'
import { MobileWebBrowserStreams } from './mobile-web-browser-streams'
import {
isRetryableMobileWebBridgeError,
mobileWebBridgeErrorCode
@@ -16,9 +15,6 @@ import {
mobileWebBridgeRequestMessage
} from './mobile-web-bridge-roundtrip-fixture'
import { MobileWebWorkspaceSubscriptions } from './mobile-web-workspace-subscriptions'
import { MobileWebWorkspaceAuthority } from './mobile-web-workspace-authority'
const randomBytes = (length: number): Uint8Array => new Uint8Array(length).fill(4)
function stubClient(): RpcClient {
return { subscribe: vi.fn(() => () => {}) } as unknown as RpcClient
@@ -29,20 +25,9 @@ function ledgerStarters(): { name: string; start: (subscriptionId: string) => vo
const postEvent = async (): Promise<void> => {}
const isActive = (): boolean => true
const client = stubClient()
const workspaceAuthority = new MobileWebWorkspaceAuthority(randomBytes)
workspaceAuthority.synchronize([{ workspaceId: 'host-workspace', repoId: 'repo-1' }])
const pageWorkspaceId = workspaceAuthority.pageWorkspaceId('host-workspace')
const pageId = 'raw-page'
const postClosed = (): void => {}
const account = new MobileWebAccountSubscriptions({ isActive, postEvent, postClosed })
const workspace = new MobileWebWorkspaceSubscriptions({ isActive, postEvent, postClosed })
const browser = new MobileWebBrowserStreams({
isActive,
workspaceAuthority,
postEvent,
postClosed
})
return [
{
name: 'account',
@@ -51,25 +36,6 @@ function ledgerStarters(): { name: string; start: (subscriptionId: string) => vo
{
name: 'workspace',
start: (subscriptionId) => workspace.start({ requestId: 'r', subscriptionId, client })
},
{
name: 'browser',
start: (subscriptionId) =>
browser.start({
requestId: 'r',
subscriptionId,
payload: {
workspaceId: pageWorkspaceId,
pageId,
format: 'jpeg',
quality: 72,
maxWidth: 800,
maxHeight: 600,
everyNthFrame: 1,
minFrameIntervalMs: 100
},
client
})
}
]
}
@@ -167,16 +167,6 @@ export const MOBILE_WEB_BRIDGE_OPERATIONS = {
manageReview: 'mutation',
submitReview: 'mutation'
},
browser: {
subscribe: 'subscription',
navigate: 'mutation',
back: 'mutation',
forward: 'mutation',
reload: 'mutation',
dialog: 'mutation',
pointer: 'mutation',
keyboard: 'mutation'
},
account: {
snapshot: 'read',
select: 'mutation',