Files
orca/src/main/ipc/ssh.ts
T
OrcaWinandm4air 5f308bfa9c revert: take the 26 Phase 3 (#16741 port) PRs back out of main (#24559)
* Revert "feat(orcad): source-side dormant export of a relay-hosted SSH target (#16741 T6-8) (#24519)"

This reverts commit 783101b304.

* Revert "feat(ssh): update, roll back, recover and stop a managed orcad server (#16741 T6-5 follow-up) (#24463)"

This reverts commit 38c2d1dcb9.

* Revert "feat(ssh): deploy and pair an empty managed orcad server over SSH (#16741 T6-5) (#24453)"

This reverts commit 8b76683b40.

* Revert "fix(ssh): orcad GC honors the activation journal; readiness requires proven daemon coverage (#16741 T6 follow-up) (#24451)"

This reverts commit d3f8c5063b.

* Revert "feat(ssh): remote orcad stop by request file and journaled decommission (#16741 T6-4) (#24449)"

This reverts commit 43d9b43d3f.

* Revert "feat(orcad): supervisable server: stop requests, managed stop receipts and a lifetime that keeps its lock on failed teardown (#16741 T6-3) (#24433)"

This reverts commit b093d3ab20.

* Revert "feat(ssh): crash-safe orcad activation, rollback and recovery (#16741 T6-2) (#24423)"

This reverts commit 1a9ac0e955.

* Revert "feat(runtime): SSH access links for paired servers in a downgrade-safe sidecar (#16741 T5-1+T5-2) (#24420)"

This reverts commit 99db2bfae4.

* Revert "feat(relay): capability-gated owner reset with a durable preparation journal (#16741 T3 R1) (#24418)"

This reverts commit 34a582bd39.

* Revert "feat(ssh): track connection-manager drains, test probes and provider continuations (#16741 T2 P3+P8a) (#24407)"

This reverts commit d53063d2b1.

* Revert "feat(daemon): idle retirement, session census and recovery-only provider (#16741 T2 P4b) (#24409)"

This reverts commit ff212dbbef.

* Revert "feat(ssh): add pty.resumeClient and split SSH PTY process listing (#16741 T2 P5+P6) (#24414)"

This reverts commit 92cb71765e.

* Revert "feat(relay): await owned watcher and agent children on shutdown (#16741 T2 P1) (#24400)"

This reverts commit 6b36e4f85b.

* Revert "feat(session): retry failed renderer session writes and verify local folder PTYs (#16741 T2 P9) (#24406)"

This reverts commit d23ecef301.

* Revert "feat(ssh): remote orcad primitives on the pinned Node runtime (#16741 T6-1) (#24419)"

This reverts commit dd87ae578d.

* Revert "fix(runtime): fence runtime-environment subscriptions and status probes by identity (#16741 T5-3) (#24421)"

This reverts commit ece9e4d2e3.

* Revert "feat(orcad): migration manifest and dormant-state contracts (#16741 T6-7) (#24422)"

This reverts commit 3fbdaba262.

* Revert "feat(ssh): wire SshConnection through the work and transport close ledgers (#16741 T2 P2) (#24401)"

This reverts commit 4e8edc8872.

* Revert "feat(profiles): carry markdown frontmatter visibility in project transfers (#16741 T2 P7) (#24405)"

This reverts commit 60c93263cc.

* Revert "fix(runtime): project the PTY incarnation onto mobile session tabs (#24413)"

This reverts commit 99e0303572.

* Revert "feat(daemon): tag daemon stream data with the PTY incarnation id (#16741 T2 P4a) (#24402)"

This reverts commit 817af768b0.

* Revert "feat(ssh): port the SSH connection work ledger and transport close ledger (#16741 T2) (#24210)"

This reverts commit c9918931c8.

* Revert "feat(relay): fence and drain file and git response streams on shutdown (#24185)"

This reverts commit dc08ffeba9.

* Revert "refactor(runtime-rpc): extract the Node WebSocket lifecycle; opt-in pinned port (#24186)"

This reverts commit a789233bbb.

* Revert "feat(relay): route relay handlers through work admission; producer publication drain (#24181)"

This reverts commit 0b812bd698.

* Revert "feat(relay): land the #16741 T1 seam (work drain, publication drain, release gate) (#24156)"

This reverts commit 3aa2d3af7c.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-02 00:52:32 -07:00

270 lines
9.0 KiB
TypeScript

import {
AiVaultSearchRequestSchema,
AiVaultSearchStatusRequestSchema
} from '../../shared/ai-vault-search-contract'
import { ipcMain, type BrowserWindow } from 'electron'
import type { Store } from '../persistence'
import { SshConnectionStore } from '../ssh/ssh-connection-store'
import { SshConnectionManager } from '../ssh/ssh-connection-manager'
import type { SshRelayAiVaultHostInfo } from '../ssh/ssh-relay-session'
import type {
SshAiVaultRelayListParams,
SshAiVaultRelayTitleParams
} from '../../shared/ssh-ai-vault-relay'
import { SshPortForwardManager } from '../ssh/ssh-port-forward'
import { isRuntimeOwnedSshTargetId } from '../../shared/execution-host'
import { quitTeardownStartGate } from '../quit-teardown-start-gate'
import {
getSshTargetRegistryStore,
setSshConnectionManagerResolver,
setSshTargetRegistryHandlers,
setSshTargetRegistryStore
} from '../ssh/ssh-target-registry'
// Why re-exported: the registry moved to ../ssh/ssh-target-registry so the runtime can
// read it without pulling ipcMain in, but many existing importers reference these from
// here. Re-exporting keeps them working without a repo-wide rename.
export {
connectRegisteredSshTarget,
getActiveMultiplexer,
getRegisteredSshState,
getSshConnectionManager,
listRegisteredRemovedSshTargetLabels,
listRegisteredSshTargets
} from '../ssh/ssh-target-registry'
import { registerSshBrowseHandler } from './ssh-browse'
import { registerCredentialHandler } from './ssh-passphrase'
import type { OrcaRuntimeService } from '../runtime/orca-runtime'
import {
initializeSshConnectionGenerationSession,
resetSshConnectionGenerations
} from '../ssh/ssh-connection-generation'
import { resetSshProviderAuthorities } from '../ssh/ssh-provider-authority'
import { activeSessions } from './ssh-active-relay-sessions'
import {
registerAdvertisedUrlRefresh,
unregisterAdvertisedUrlRefresh
} from './ssh-advertised-url-refresh'
import {
connectInFlight,
credentialRequestedForTarget,
pendingTransportReconnects,
resetRelayInFlight,
testConnectionProbes,
testingTargets
} from './ssh-connect-attempt-registry'
import { createSshConnectionCallbacks } from './ssh-connection-state-callbacks'
import { registerSshConnectionHandlers } from './ssh-connection-handlers'
import {
registerPowerMonitorReconnect,
unregisterPowerMonitorReconnect
} from './ssh-host-sleep-reconnect'
import {
connectionManager,
getCurrentMainWindow,
portForwardManager,
setConnectionManager,
setCurrentGetMainWindow,
setCurrentRuntime,
setPersistedStore,
setPortForwardManager
} from './ssh-ipc-context'
import { registerSshPortForwardHandlers } from './ssh-port-forward-handlers'
import { persistPortForwardsWithUnrestored } from './ssh-port-forward-persistence'
import { clearRelayLostBackoff, relayLostBackoff } from './ssh-relay-lost-backoff'
import { refreshActiveRelaySessions } from './ssh-relay-session-callbacks'
import { broadcastPortForwards, relayStateOverrides } from './ssh-renderer-broadcast'
import { resetSshShutdownDrain } from './ssh-shutdown-drain'
import { registerSshTargetCrudHandlers } from './ssh-target-crud-handlers'
import { targetLifecycleInFlight } from './ssh-target-lifecycle-queue'
const SSH_IPC_CHANNELS = [
'ssh:listTargets',
'ssh:listRemovedTargetLabels',
'ssh:addTarget',
'ssh:updateTarget',
'ssh:removeTarget',
'ssh:importConfig',
'ssh:listConfigHosts',
'ssh:resolveConfigHost',
'ssh:connect',
'ssh:disconnect',
'ssh:terminateSessions',
'ssh:resetRelay',
'ssh:getState',
'ssh:needsPassphrasePrompt',
'ssh:testConnection',
'ssh:addPortForward',
'ssh:updatePortForward',
'ssh:removePortForward',
'ssh:listPortForwards',
'ssh:listDetectedPorts'
] as const
export function getActiveSshAiVaultHostInfo(targetId: string): SshRelayAiVaultHostInfo | null {
if (isRuntimeOwnedSshTargetId(targetId)) {
return null
}
return activeSessions.get(targetId)?.getAiVaultHostInfo() ?? null
}
export function getActiveSshAiVaultHostInfos(): SshRelayAiVaultHostInfo[] {
return [...activeSessions.values()].flatMap((session) => {
if (isRuntimeOwnedSshTargetId(session.targetId)) {
return []
}
const info = session.getAiVaultHostInfo()
return info ? [info] : []
})
}
export async function requestActiveSshSessionSearch(
targetId: string,
method: string,
params: unknown
): Promise<unknown> {
if (isRuntimeOwnedSshTargetId(targetId)) {
throw new Error('SSH target belongs to another runtime')
}
const session = activeSessions.get(targetId)
if (!session) {
throw new Error('SSH relay is not ready')
}
if (method === 'aiVault.searchSessions') {
return session.requestSessionSearch(method, AiVaultSearchRequestSchema.parse(params))
}
if (method === 'aiVault.searchStatus') {
return session.requestSessionSearch(method, AiVaultSearchStatusRequestSchema.parse(params))
}
throw new Error('Unknown session search method')
}
export async function requestActiveSshAiVaultSessionList(
targetId: string,
params: SshAiVaultRelayListParams,
options: { signal?: AbortSignal; timeoutMs?: number } = {}
): Promise<unknown> {
if (isRuntimeOwnedSshTargetId(targetId)) {
return null
}
const session = activeSessions.get(targetId)
if (!session) {
throw new Error('SSH relay is not ready')
}
return session.requestAiVaultSessionList(params, options)
}
export async function requestActiveSshAiVaultSessionTitles(
targetId: string,
params: SshAiVaultRelayTitleParams,
options: { signal?: AbortSignal; timeoutMs?: number } = {}
): Promise<unknown> {
if (isRuntimeOwnedSshTargetId(targetId)) {
return null
}
const session = activeSessions.get(targetId)
if (!session) {
throw new Error('SSH relay is not ready')
}
return session.requestAiVaultSessionTitles(params, options)
}
export function registerSshHandlers(
store: Store,
getMainWindow: () => BrowserWindow | null,
runtime?: OrcaRuntimeService
): { connectionManager: SshConnectionManager; sshStore: SshConnectionStore } {
initializeSshConnectionGenerationSession()
// Why: macOS re-activation re-calls this with a new BrowserWindow; ipcMain.handle() throws on a duplicate channel, so remove prior handlers first.
for (const ch of SSH_IPC_CHANNELS) {
ipcMain.removeHandler(ch)
}
setCurrentGetMainWindow(getMainWindow)
setCurrentRuntime(runtime)
setSshTargetRegistryStore(new SshConnectionStore(store))
setPersistedStore(store)
registerAdvertisedUrlRefresh(getCurrentMainWindow)
registerCredentialHandler()
const callbacks = createSshConnectionCallbacks()
if (connectionManager) {
connectionManager.setCallbacks(callbacks)
} else {
setConnectionManager(new SshConnectionManager(callbacks))
}
setPortForwardManager(portForwardManager ?? new SshPortForwardManager())
portForwardManager!.setCallbacks({
onForwardClosed: (entry, reason) => {
if (reason.kind === 'unexpected-exit') {
console.warn(
`[ssh] Port forward ${entry.localPort} → ${entry.remoteHost}:${entry.remotePort} closed unexpectedly${
reason.detail ? `: ${reason.detail}` : ''
}`
)
}
persistPortForwardsWithUnrestored(entry.connectionId)
broadcastPortForwards(getCurrentMainWindow, entry.connectionId)
}
})
refreshActiveRelaySessions()
registerPowerMonitorReconnect()
registerSshBrowseHandler(() => connectionManager)
setSshConnectionManagerResolver(() => connectionManager)
registerSshTargetCrudHandlers()
registerSshConnectionHandlers()
registerSshPortForwardHandlers()
return {
connectionManager: connectionManager!,
sshStore: getSshTargetRegistryStore() as SshConnectionStore
}
}
export async function resetSshHandlerStateForTests(): Promise<void> {
unregisterAdvertisedUrlRefresh()
unregisterPowerMonitorReconnect()
for (const ch of SSH_IPC_CHANNELS) {
ipcMain.removeHandler(ch)
}
ipcMain.removeHandler('ssh:submitCredential')
// Why: allSettled — a rejected disposal write must not abort the rest of the reset and leak state into the next test.
await Promise.allSettled(
[...activeSessions.values()].map((session) => session.disposeAndPersist())
)
activeSessions.clear()
for (const targetId of relayLostBackoff.keys()) {
clearRelayLostBackoff(targetId)
}
relayStateOverrides.clear()
connectInFlight.clear()
targetLifecycleInFlight.clear()
pendingTransportReconnects.clear()
resetSshConnectionGenerations()
resetSshProviderAuthorities()
resetRelayInFlight.clear()
testingTargets.clear()
testConnectionProbes.clear()
credentialRequestedForTarget.clear()
quitTeardownStartGate.resetForTests()
resetSshShutdownDrain()
await connectionManager?.disconnectAll()
portForwardManager?.dispose()
setConnectionManager(null)
setSshConnectionManagerResolver(null)
setPortForwardManager(null)
setSshTargetRegistryStore(null)
setPersistedStore(null)
setSshTargetRegistryHandlers({ connect: null, getState: null })
setCurrentGetMainWindow(() => null)
setCurrentRuntime(undefined)
}
export function getSshConnectionStore(): SshConnectionStore | null {
return getSshTargetRegistryStore()
}