diff --git a/.oxlintrc.json b/.oxlintrc.json index ec172c5e108..1f412df859c 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -212,6 +212,39 @@ "rules": { "max-lines": ["error", { "max": 800, "skipBlankLines": true, "skipComments": true }] } + }, + { + "files": [ + "src/main/runtime/rpc/dispatcher.ts", + "src/main/plugins/plugin-runtime-rpc.ts", + "src/main/plugins/plugin-runtime-binding.ts" + ], + "rules": { + "no-restricted-imports": [ + "error", + { + "paths": [ + { + "name": "@linear/sdk", + "allowTypeImports": true, + "message": "Value-importing @linear/sdk hoists its ~2.6MB CJS bundle into the eager top-level require block and defeats the lazy loader. Use `import type` plus loadLinearSdk() from src/main/linear/linear-sdk.ts." + }, + { + "name": "ssh2", + "allowTypeImports": true, + "message": "Value-importing ssh2 hoists it into the eager main-process require block and blocks startup (#35731). Use `import type` plus loadSsh2() from src/main/ssh/ssh2-module-load.ts." + } + ], + "patterns": [ + { + "group": ["**/rpc-method-registry"], + "allowTypeImports": true, + "message": "The registry imports every RPC method (~3.4k modules); take the method list as a parameter so importers stay light." + } + ] + } + ] + } } ], "ignorePatterns": [ diff --git a/src/main/plugins/plugin-runtime-binding.ts b/src/main/plugins/plugin-runtime-binding.ts index 8981874d716..6c904680d46 100644 --- a/src/main/plugins/plugin-runtime-binding.ts +++ b/src/main/plugins/plugin-runtime-binding.ts @@ -7,6 +7,7 @@ import { installPluginHookRunner } from './hooks/plugin-hook-participants' import { installPluginResidentWorkerWarmer } from './workers/plugin-resident-worker' import type { OrcaRuntimeService } from '../runtime/orca-runtime' import { pluginRuntimeRpc } from './plugin-runtime-rpc' +import type { DispatcherOptions } from '../runtime/rpc/dispatcher' import type { PluginRuntimeRpc } from './workers/plugin-host-workspace-method-bindings' import { pluginTerminalTitle, @@ -47,10 +48,11 @@ export function pluginRuntimeDelegate( export function bindPluginRuntime( service: PluginService, runtime: OrcaRuntimeService, - hookServer: { getStatusSnapshot(): readonly PluginAgentStatusRow[] } + hookServer: { getStatusSnapshot(): readonly PluginAgentStatusRow[] }, + methods: DispatcherOptions['methods'] ): PluginRuntimeDelegate { const statuses = () => hookServer.getStatusSnapshot() - const delegate = pluginRuntimeDelegate(runtime, statuses, pluginRuntimeRpc(runtime)) + const delegate = pluginRuntimeDelegate(runtime, statuses, pluginRuntimeRpc(runtime, methods)) service.setRuntimeDelegate(delegate) installPluginHookRunner(service, runtime) installPluginResidentWorkerWarmer(service) diff --git a/src/main/plugins/plugin-runtime-rpc.ts b/src/main/plugins/plugin-runtime-rpc.ts index 59504bd8cc6..0bb9e9eeb92 100644 --- a/src/main/plugins/plugin-runtime-rpc.ts +++ b/src/main/plugins/plugin-runtime-rpc.ts @@ -19,11 +19,11 @@ export class PluginRuntimeRpcError extends Error { */ export function pluginRuntimeRpc( runtime: OrcaRuntimeService, - methods?: DispatcherOptions['methods'] + methods: DispatcherOptions['methods'] ): PluginRuntimeRpc { let dispatcher: RpcDispatcher | null = null return async (method, params, options = {}) => { - dispatcher ??= new RpcDispatcher({ runtime, ...(methods ? { methods } : {}) }) + dispatcher ??= new RpcDispatcher({ runtime, methods }) // The dispatcher maps a throw to an error answer; keep the original so its code reaches the plugin. let refusal: unknown = null const response = await dispatcher.dispatch( diff --git a/src/main/runtime/rpc/dispatcher.ts b/src/main/runtime/rpc/dispatcher.ts index 3ba7f9a6de7..ba51225c7df 100644 --- a/src/main/runtime/rpc/dispatcher.ts +++ b/src/main/runtime/rpc/dispatcher.ts @@ -11,7 +11,6 @@ import { } from './core' import { successResponse } from './errors' -import { ALL_RPC_METHODS } from './methods/rpc-method-registry' import { withoutMethodsThisHostDoesNotServe } from './methods/terminal/session-terminal-scrollback' import { emulatorProbe, emulatorProbeError } from '../../emulator/emulator-probe' import type { OrcaRuntimeService } from '../orca-runtime' @@ -37,7 +36,7 @@ import { resolveOrcadFeatureRequirement, rpcMethodFeatureWait } from './orcad-fe export type DispatcherOptions = { runtime: OrcaRuntimeService - methods?: readonly RpcAnyMethodDeclaration[] + methods: readonly RpcAnyMethodDeclaration[] /** Pins every call to this scope, for in-process bridges that relay a non-owner caller. */ callerScope?: RpcCallerScope } @@ -56,7 +55,7 @@ export class RpcDispatcher { private readonly legacyOrchestration: OrchestrationLegacyCompatibility private readonly pinnedCallerScope: RpcCallerScope | undefined - constructor({ runtime, methods = ALL_RPC_METHODS, callerScope }: DispatcherOptions) { + constructor({ runtime, methods, callerScope }: DispatcherOptions) { this.runtime = runtime this.pinnedCallerScope = callerScope this.registry = buildRegistry(withoutMethodsThisHostDoesNotServe(methods)) diff --git a/src/main/runtime/rpc/rpc-caller-scope.test.ts b/src/main/runtime/rpc/rpc-caller-scope.test.ts index 27c03695e2c..b571d54d425 100644 --- a/src/main/runtime/rpc/rpc-caller-scope.test.ts +++ b/src/main/runtime/rpc/rpc-caller-scope.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest' import { OrcaRuntimeService } from '../orca-runtime' import type { RpcRequest, RpcResponse } from './core' import { RpcDispatcher } from './dispatcher' +import { ALL_RPC_METHODS } from './methods/rpc-method-registry' import type { RpcCallerScope } from './rpc-caller-scope' const PAIRED: RpcCallerScope = { kind: 'runtime-paired', grants: [] } @@ -16,7 +17,7 @@ async function dispatchAs( params?: unknown, runtime = new OrcaRuntimeService() ): Promise { - return new RpcDispatcher({ runtime }).dispatch( + return new RpcDispatcher({ runtime, methods: ALL_RPC_METHODS }).dispatch( request(method, params), scope ? { callerScope: scope } : undefined ) @@ -50,6 +51,7 @@ describe('a pinned in-process bridge dispatcher', () => { it('ignores a wider per-call scope', async () => { const dispatcher = new RpcDispatcher({ runtime: new OrcaRuntimeService(), + methods: ALL_RPC_METHODS, callerScope: PAIRED }) const response = await dispatcher.dispatch(request('computer.click', { app: 'Finder' }), { diff --git a/src/main/startup/main-process-plugins.ts b/src/main/startup/main-process-plugins.ts index c8dbbfacf28..624cf2a8c81 100644 --- a/src/main/startup/main-process-plugins.ts +++ b/src/main/startup/main-process-plugins.ts @@ -45,6 +45,7 @@ import { mainProcessState as state } from './main-process-state' import type { OrcaRuntimeService } from '../runtime/orca-runtime' import { PluginStatusBarState } from '../plugins/status-bar/plugin-status-bar-state' import { createDesktopPluginAppearance } from '../plugins/appearance/desktop-plugin-appearance' +import { ALL_RPC_METHODS } from '../runtime/rpc/methods/rpc-method-registry' const PLUGIN_AUTO_UPDATE_STARTUP_DELAY_MS = 60_000 // Why: the official catalog fetch must not compete with first paint; opening the marketplace seeds sooner. @@ -240,7 +241,7 @@ export async function initializeMainProcessPlugins(runtime: OrcaRuntimeService): } }) // Bound here, not in the IPC handlers: a headless serve never opens a window to register them. - const pluginDelegate = bindPluginRuntime(pluginService, runtime, agentHookServer) + const pluginDelegate = bindPluginRuntime(pluginService, runtime, agentHookServer, ALL_RPC_METHODS) bindPluginRpcServices(store, pluginService, { marketplace: state.pluginMarketplaceService, installer: state.pluginMarketplaceInstaller diff --git a/src/main/startup/main-process-ready-runtime.ts b/src/main/startup/main-process-ready-runtime.ts index 4244f547719..c892bee7611 100644 --- a/src/main/startup/main-process-ready-runtime.ts +++ b/src/main/startup/main-process-ready-runtime.ts @@ -44,6 +44,7 @@ import { runGatedWslMoveDesktopRewritePass } from '../persistence/moving-wsl-pro import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../shared/layout-owner-switch' import { repairDuplicatePtyPanesOnStart } from '../runtime/moved-pane-owner-rekey' import { WslUsageReporter } from '../telemetry/wsl-usage-reporter' +import { ALL_RPC_METHODS } from '../runtime/rpc/methods/rpc-method-registry' // Headless serve has no first window; timer fallback must still sweep trash. const WORKTREE_TRASH_SWEEP_FALLBACK_MS = 15_000 @@ -73,7 +74,7 @@ export async function initializeReadyRuntimeServices(): Promise { runtime.setAgentBrowserBridge(state.agentBrowserBridge) // Reclaim browser daemons orphaned by prior crashes/SIGKILL. void state.agentBrowserBridge.sweepOrphanedSessions() - const browserClientAutomationDispatcher = new RpcDispatcher({ runtime }) + const browserClientAutomationDispatcher = new RpcDispatcher({ runtime, methods: ALL_RPC_METHODS }) configureBrowserClientPageAutomationRuntime({ browserManager, getAgentBrowserBridge: () => state.agentBrowserBridge,