mirror of
https://github.com/stablyai/orca.git
synced 2026-09-30 16:02:56 +00:00
refactor(mobile): brand the host-side workspace and repo identifiers
The authority now mints branded MobileWebHostWorkspaceId and MobileWebHostRepoId, so passing a page handle where a resolved host identifier belongs is a compile error. Reauthorization is the case that matters: assertHostWorkspaceBinding no longer accepts the page handle as its expected host value. Values the host itself reported go through two named boundary functions rather than a bare cast, so every such site is greppable. No runtime check changed. The page-side handles stay unbranded: branding MobileWebWorkspaceIdSchema alone produced 527 type errors across the page and shell, and six more handle types remain. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
This commit is contained in:
@@ -20,7 +20,10 @@ import type { RpcClient } from '../transport/rpc-client'
|
||||
import type { RpcFailure } from '../transport/types'
|
||||
import { MobileWebBrokerError } from './mobile-web-broker-error'
|
||||
import type { MobileWebNativeCapabilityAuthority } from './mobile-web-native-capability-authority'
|
||||
import type { MobileWebWorkspaceAuthority } from './mobile-web-workspace-authority'
|
||||
import type {
|
||||
MobileWebHostWorkspaceId,
|
||||
MobileWebWorkspaceAuthority
|
||||
} from './mobile-web-workspace-authority'
|
||||
|
||||
type MarkdownOperationArgs = {
|
||||
operation: string
|
||||
@@ -102,7 +105,7 @@ export async function executeMobileWebMarkdownOperation(
|
||||
async function verifyMarkdownTarget(
|
||||
args: MarkdownOperationArgs,
|
||||
target: MarkdownTarget
|
||||
): Promise<string> {
|
||||
): Promise<MobileWebHostWorkspaceId> {
|
||||
const hostWorkspaceId = args.workspaceAuthority.hostWorkspaceId(target.workspaceId)
|
||||
const response = await args.client.sendRequest('session.tabs.list', {
|
||||
worktree: `id:${hostWorkspaceId}`
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { MobileWebHostWorkspaceId } from './mobile-web-workspace-authority'
|
||||
import { MobileWebBrokerError } from './mobile-web-broker-error'
|
||||
import { MOBILE_WEB_NATIVE_CHAT_IMAGE_LIMIT } from '../../../src/shared/mobile-web/native-chat-operation-contract'
|
||||
|
||||
export type MobileWebHostNativeChatBinding = {
|
||||
hostWorkspaceId: string
|
||||
hostWorkspaceId: MobileWebHostWorkspaceId
|
||||
hostTabId: string
|
||||
hostTerminalId: string | null
|
||||
agent: string
|
||||
|
||||
@@ -21,7 +21,10 @@ import { getRepoIdFromMobileWorktreeId } from '../session/mobile-session-route-h
|
||||
import { loadMobileNewTabAgentOptions } from '../session/mobile-new-tab-agent-loader'
|
||||
import type { RpcClient } from '../transport/rpc-client'
|
||||
import { MobileWebBrokerError } from './mobile-web-broker-error'
|
||||
import type { MobileWebWorkspaceAuthority } from './mobile-web-workspace-authority'
|
||||
import type {
|
||||
MobileWebHostWorkspaceId,
|
||||
MobileWebWorkspaceAuthority
|
||||
} from './mobile-web-workspace-authority'
|
||||
|
||||
export async function executeMobileWebSessionQuickCommandOperation(args: {
|
||||
operation: string
|
||||
@@ -99,7 +102,7 @@ async function readQuickCommands(client: RpcClient): Promise<TerminalQuickComman
|
||||
|
||||
function projectQuickCommands(
|
||||
result: unknown,
|
||||
hostWorkspaceId: string,
|
||||
hostWorkspaceId: MobileWebHostWorkspaceId,
|
||||
pageWorkspaceId: string
|
||||
): MobileWebQuickCommandSnapshotResult {
|
||||
const commands = parseNormalizedTerminalQuickCommands(
|
||||
@@ -127,7 +130,7 @@ function projectQuickCommands(
|
||||
function hostMutation(
|
||||
mutation: MobileWebQuickCommandMutationPayload['mutation'],
|
||||
commands: TerminalQuickCommand[],
|
||||
hostWorkspaceId: string,
|
||||
hostWorkspaceId: MobileWebHostWorkspaceId,
|
||||
pageWorkspaceId: string
|
||||
): TerminalQuickCommandMutation {
|
||||
const repoId = workspaceRepoId(hostWorkspaceId)
|
||||
@@ -161,7 +164,7 @@ function hostMutation(
|
||||
async function launchQuickCommand(args: {
|
||||
client: RpcClient
|
||||
command: TerminalQuickCommand
|
||||
hostWorkspaceId: string
|
||||
hostWorkspaceId: MobileWebHostWorkspaceId
|
||||
pageWorkspaceId: string
|
||||
requestId: string
|
||||
workspaceAuthority: MobileWebWorkspaceAuthority
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { MobileWebHostWorkspaceId } from './mobile-web-workspace-authority'
|
||||
import {
|
||||
AGENT_STATUS_ASSISTANT_MESSAGE_MAX_LENGTH,
|
||||
AGENT_STATUS_INTERACTIVE_PROMPT_MAX_LENGTH,
|
||||
@@ -23,7 +24,7 @@ const TAB_TYPES = ['terminal', 'markdown', 'file', 'browser'] as const
|
||||
|
||||
export function mobileWebSessionSnapshot(
|
||||
result: unknown,
|
||||
hostWorkspaceId: string,
|
||||
hostWorkspaceId: MobileWebHostWorkspaceId,
|
||||
pageWorkspaceId: string,
|
||||
browserAuthority: MobileWebBrowserAuthority,
|
||||
nativeChatAuthority: MobileWebNativeChatAuthority
|
||||
@@ -101,7 +102,7 @@ function isActiveSessionTab(value: unknown): boolean {
|
||||
|
||||
function mobileWebSessionTab(
|
||||
value: unknown,
|
||||
hostWorkspaceId: string,
|
||||
hostWorkspaceId: MobileWebHostWorkspaceId,
|
||||
browserAuthority: MobileWebBrowserAuthority,
|
||||
nativeChatAuthority: MobileWebNativeChatAuthority
|
||||
): MobileWebSessionTab | null {
|
||||
@@ -236,7 +237,7 @@ function safeNonnegativeInteger(value: unknown): number | undefined {
|
||||
|
||||
function mobileWebNativeChatBinding(
|
||||
value: unknown,
|
||||
hostWorkspaceId: string
|
||||
hostWorkspaceId: MobileWebHostWorkspaceId
|
||||
): MobileWebHostNativeChatBinding | null {
|
||||
if (
|
||||
!isRecord(value) ||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { MobileWebHostWorkspaceId } from './mobile-web-workspace-authority'
|
||||
import { MOBILE_WEB_BRIDGE_MAX_SUBSCRIPTIONS } from '../../../src/shared/mobile-web/bridge-contract'
|
||||
import {
|
||||
MOBILE_WEB_SESSION_EVENT_MAX_BYTES,
|
||||
@@ -37,7 +38,7 @@ export class MobileWebSessionSubscriptions {
|
||||
requestId: string
|
||||
subscriptionId: string
|
||||
pageWorkspaceId: string
|
||||
hostWorkspaceId: string
|
||||
hostWorkspaceId: MobileWebHostWorkspaceId
|
||||
client: RpcClient
|
||||
}): void {
|
||||
if (this.records.has(args.subscriptionId)) {
|
||||
@@ -121,7 +122,7 @@ export class MobileWebSessionSubscriptions {
|
||||
private receive(
|
||||
subscriptionId: string,
|
||||
record: SubscriptionRecord,
|
||||
hostWorkspaceId: string,
|
||||
hostWorkspaceId: MobileWebHostWorkspaceId,
|
||||
pageWorkspaceId: string,
|
||||
event: unknown
|
||||
): void {
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { MobileWebWorkspaceAuthority } from './mobile-web-workspace-authority'
|
||||
import {
|
||||
mobileWebHostWorkspaceIdFromHost,
|
||||
MobileWebWorkspaceAuthority
|
||||
} from './mobile-web-workspace-authority'
|
||||
|
||||
export function createMobileWebWorkspaceAuthorityFixture(
|
||||
pageWorkspaceId = 'workspace-1',
|
||||
@@ -12,7 +15,7 @@ export function createMobileWebWorkspaceAuthorityFixture(
|
||||
if (candidate !== pageWorkspaceId) {
|
||||
throw new Error('not_found')
|
||||
}
|
||||
return hostWorkspaceId
|
||||
return mobileWebHostWorkspaceIdFromHost(hostWorkspaceId)
|
||||
}
|
||||
}
|
||||
return authority
|
||||
|
||||
@@ -7,6 +7,24 @@ import {
|
||||
import { getProjectIdentityKey } from '../../../src/shared/project-host-setup-projection'
|
||||
import type { NewWorkspaceRepository } from '../worktree/host-workspace-creation-operations'
|
||||
|
||||
declare const hostWorkspaceIdBrand: unique symbol
|
||||
declare const hostRepoIdBrand: unique symbol
|
||||
|
||||
/** Host-side identifiers the authority mints from an opaque page handle. Branding keeps a page
|
||||
* handle from being passed back in as if it were already resolved. */
|
||||
export type MobileWebHostWorkspaceId = string & { readonly [hostWorkspaceIdBrand]: true }
|
||||
export type MobileWebHostRepoId = string & { readonly [hostRepoIdBrand]: true }
|
||||
|
||||
/** A value the host itself reported as a host identifier, so it never came from the page. Every
|
||||
* use is a boundary annotation, not a conversion of a page handle. */
|
||||
export function mobileWebHostWorkspaceIdFromHost(value: string): MobileWebHostWorkspaceId {
|
||||
return value as MobileWebHostWorkspaceId
|
||||
}
|
||||
|
||||
export function mobileWebHostRepoIdFromHost(value: string): MobileWebHostRepoId {
|
||||
return value as MobileWebHostRepoId
|
||||
}
|
||||
|
||||
export type MobileWebHostWorkspaceBinding = {
|
||||
workspaceId: string
|
||||
repoId: string
|
||||
@@ -63,15 +81,15 @@ export class MobileWebWorkspaceAuthority {
|
||||
return pageRepoId
|
||||
}
|
||||
|
||||
hostRepoId(pageRepoId: string): string {
|
||||
hostRepoId(pageRepoId: string): MobileWebHostRepoId {
|
||||
const hostRepoId = this.hostRepoIdByPageId.get(pageRepoId)
|
||||
if (!hostRepoId) {
|
||||
throw new MobileWebBrokerError('not_found')
|
||||
}
|
||||
return hostRepoId
|
||||
return hostRepoId as MobileWebHostRepoId
|
||||
}
|
||||
|
||||
assertHostRepoBinding(pageRepoId: string, expectedHostRepoId: string): void {
|
||||
assertHostRepoBinding(pageRepoId: string, expectedHostRepoId: MobileWebHostRepoId): void {
|
||||
if (this.hostRepoId(pageRepoId) !== expectedHostRepoId) {
|
||||
throw new MobileWebBrokerError('conflict')
|
||||
}
|
||||
@@ -119,15 +137,18 @@ export class MobileWebWorkspaceAuthority {
|
||||
return connectionId
|
||||
}
|
||||
|
||||
hostWorkspaceId(pageWorkspaceId: string): string {
|
||||
hostWorkspaceId(pageWorkspaceId: string): MobileWebHostWorkspaceId {
|
||||
const hostWorkspaceId = this.hostWorkspaceIdByPageId.get(pageWorkspaceId)
|
||||
if (!hostWorkspaceId) {
|
||||
throw new MobileWebBrokerError('not_found')
|
||||
}
|
||||
return hostWorkspaceId
|
||||
return hostWorkspaceId as MobileWebHostWorkspaceId
|
||||
}
|
||||
|
||||
assertHostWorkspaceBinding(pageWorkspaceId: string, expectedHostWorkspaceId: string): void {
|
||||
assertHostWorkspaceBinding(
|
||||
pageWorkspaceId: string,
|
||||
expectedHostWorkspaceId: MobileWebHostWorkspaceId
|
||||
): void {
|
||||
if (this.hostWorkspaceId(pageWorkspaceId) !== expectedHostWorkspaceId) {
|
||||
throw new MobileWebBrokerError('conflict')
|
||||
}
|
||||
|
||||
@@ -13,7 +13,10 @@ import { normalizeWorkspaceAgent } from '../tasks/workspace-agent-selection'
|
||||
import { nativeHostWorkspaceCreationOperations } from '../worktree/native-host-workspace-creation-operations'
|
||||
import { MobileWebBrokerError } from './mobile-web-broker-error'
|
||||
import { requireRecentUserGesture } from './mobile-web-user-gesture-requirement'
|
||||
import type { MobileWebWorkspaceAuthority } from './mobile-web-workspace-authority'
|
||||
import {
|
||||
mobileWebHostRepoIdFromHost,
|
||||
type MobileWebWorkspaceAuthority
|
||||
} from './mobile-web-workspace-authority'
|
||||
|
||||
export async function executeMobileWebWorkspaceCreationCreateOperation(args: {
|
||||
operation: string
|
||||
@@ -73,7 +76,10 @@ function assertSelectionRepoBinding(
|
||||
) {
|
||||
throw new MobileWebBrokerError('conflict')
|
||||
}
|
||||
authority.assertHostRepoBinding(pageSelection.item.repoId, hostSelection.item.repoId)
|
||||
authority.assertHostRepoBinding(
|
||||
pageSelection.item.repoId,
|
||||
mobileWebHostRepoIdFromHost(hostSelection.item.repoId)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user