Files
orca/src/shared/orcad-managed-runtime.ts
T
Neil ccedfe312e fix(ssh): move enabled SSH automations to the host's Orca server (#34044)
* fix(ssh): move enabled SSH automations to the host's Orca server

Connecting an SSH host moves its saved state to the host's managed Orca
server. An enabled SSH automation refused that move on every Connect
("automations (1), automation runs (4)"), leaving the host unconnectable;
Move anyway connected it but stranded the automation on the desktop, where
it no longer ran.

An enabled automation whose runs have all finished now moves with its run
history. The manifest still carries it disabled, which every server version
accepts; once the move commits the desktop disables its retained copy and
turns the server's copy back on through automation.update, re-sending its
start time so the next run is the next future occurrence. A server that
can't is noted in Settings; an unreachable one is retried on the next
Connect. While a move fences the host, the desktop scheduler, Run now and
the launch re-check write nothing for its automations, so a tick can no
longer break the fenced export; releasing the fence resumes them. A run
still in flight keeps blocking and the refusal now says to wait for it.

Fixes #29083

* test(ssh): mock the server automation enabler in the delta-move handler test

* test(e2e): keep the Move anyway specs refusing now that enabled automations move

The inline Move anyway spec seeded an enabled automation to force a
refusal, which this branch now moves instead. It seeds one pinned to an
earlier registration of the host, which still stays behind. The relay
bridge spec's Move anyway lookup also matched the refusal toast's
button since #34041; it now takes the pane's own.
2026-10-26 16:11:06 -07:00

224 lines
8.0 KiB
TypeScript

import type { PublicKnownRuntimeEnvironment } from './runtime-environments'
import type { OrcadTerminalCensus } from './orcad-terminal-census'
import type { OrcadMigrationBlocker } from './orcad-migration/orcad-migration-preflight'
import type { OrcadMigrationSourceCutoverPhase } from './orcad-migration/orcad-migration-source-cutover'
import type { OrcadDaemonRetirementVerdict } from './orcad-stop-request'
import type { SshManagedServerLinkedProject } from './ssh-types'
import type { OrcadStaleLockEvidence } from './orcad-stale-lock-reclaim'
export const ORCAD_MANAGED_REMOTE_PORT = 6_768
/** Refused because terminals are, or may be, still running on the host. */
export type OrcadManagedRefusal = {
outcome: 'refused'
verdict: 'live' | 'unverifiable'
code: string
reason: string
/** Only saved state blocks, which a confirmed "Move anyway" leaves on this desktop. */
canLeaveBehind?: true
}
export type OrcadManagedDeferral = {
outcome: 'deferred'
candidateVersion: string
code: string
reason: string
/** False when force cannot make the rejected lifecycle transition safe. */
forceable?: boolean
}
/** The one deferral Update anyway may pass: counted terminals the daemon carries across. */
export const ORCAD_UPDATE_TERMINALS_RUNNING_CODE = 'orcad_update_terminals_running'
/** Update anyway passes counted live-terminal deferrals, preserves version admission, and requires the confirmed incumbent build. */
export type OrcadManagedPastLiveTerminals = { incumbentVersion: string | null }
export type OrcadManagedDeployResult =
| {
outcome: 'created' | 'updated' | 'already-current'
environment: PublicKnownRuntimeEnvironment
activeVersion: string
}
| OrcadManagedDeferral
export type OrcadManagedRollbackResult =
| {
outcome: 'rolled-back'
environment: PublicKnownRuntimeEnvironment
activeVersion: string
discarded: string[]
}
| { outcome: 'refused' | 'failed'; code: string; reason: string }
/** A recovery that needs the operator to accept restoring a snapshot over changed state. */
export const ORCAD_RECOVERY_CHANGED_STATE_CODE = 'orcad_recovery_changed_state'
export type OrcadManagedRecoveryResult =
| { outcome: 'none' }
| { outcome: 'pending'; code: string; reason: string }
| {
outcome: 'recovered'
resolution: 'committed' | 'restored-incumbent'
activeVersion: string | null
environment: PublicKnownRuntimeEnvironment
}
| OrcadManagedRefusal
/** Only a proven `exited` unlinks the server locally; anything less keeps it linked. */
export type OrcadManagedStopResult =
| {
outcome: 'unlinked'
verdict: 'exited'
environmentId: string
sshTargetId: string
stoppedVersion: string | null
retirement: OrcadDaemonRetirementVerdict | null
}
| OrcadManagedRefusal
/** Refuse restart with live or unverifiable terminals: the daemon outlives orcad and retains its inherited environment. */
export type OrcadManagedRestartResult =
| { outcome: 'restarted'; environmentId: string }
| { outcome: 'terminals-running'; liveSessions: number | null }
| { outcome: 'not-restarted'; reason: string }
/** A dry run, or a confirmation pinned to the lock record that dry run showed. */
export type OrcadManagedStaleLockReclaimRequest =
| { yes: false }
| { yes: true; expectNonce: string }
/** `orcad --reclaim-stale-lock` run on a managed host's active slot (#32177). */
export type OrcadManagedStaleLockReclaimResult =
| {
status: 'none' | 'eligible' | 'refused'
message: string
lockPath: string
evidence?: OrcadStaleLockEvidence
}
/** `notRestarted` says why the server did not start again once its lock was cleared. */
| {
status: 'reclaimed'
environmentId: string
message: string
lockPath: string
/** The record the reclaim acted on. */
evidence?: OrcadStaleLockEvidence
notRestarted?: string
}
/** No remote reclaim occurred. reclaimWithoutPin requires manual reclaim because the host cannot pin confirmation;
* otherwise lockPath names the lock to delete manually. */
| { status: 'unsupported'; message: string; lockPath?: string; reclaimWithoutPin?: true }
/** The slot runs the command but gave no answer this client understands; nothing is known. */
| { status: 'no-answer'; message: string }
/** Another lifecycle step holds the host; nothing changed. */
| { status: 'busy'; message: string }
/** Forget only unlinks this desktop; an unreachable server may still run, so its verdict remains unverifiable. */
export type OrcadManagedForgetResult =
| {
outcome: 'forgotten'
verdict: 'unverifiable'
environmentId: string
sshTargetId: string
}
| OrcadManagedRefusal
export type OrcadManagedCancelStopResult =
| { outcome: 'none' }
/** The stop was withdrawn before orcad acted on it; the server keeps serving. */
| { outcome: 'canceled'; activeVersion: string }
/** orcad had already exited; finish with stop to unlink the server. */
| { outcome: 'already-stopped' }
| OrcadManagedRefusal
export type OrcadManagedRuntimeStatus = {
environmentId: string
sshTargetId: string
activeVersion: string | null
previousVersion: string | null
activatedAt: string | null
rollbackAvailable: boolean
recovery:
| {
operation: 'activate'
phase: 'prepared' | 'incumbent-stopped' | 'snapshot-captured' | 'candidate-ready'
version: string
startedAt: string
}
| {
operation: 'rollback'
phase:
| 'prepared'
| 'incumbent-stopped'
| 'rescue-captured'
| 'rollback-state-restored'
| 'target-ready'
version: string
startedAt: string
}
| {
operation: 'decommission'
phase: 'prepared' | 'stop-dispatched' | 'process-exited'
version: string
startedAt: string
}
| null
/** Terminals the daemon runs; `null` counts are unverifiable and block updates and stops. */
terminals: OrcadTerminalCensus
/** An unfinished dormant migration into this server; a rollback is refused while it runs. */
migration: {
migrationId: string
phase: OrcadMigrationSourceCutoverPhase
startedAt: string
} | null
/** The last update this client deferred for this server, cleared once one goes through. */
deferredUpdate: (OrcadManagedDeferral & { deferredAt: string }) | null
/** The Orca release that activated `activeVersion`; absent when unknown or from older desktops. */
activeAppVersion?: string | null
/** This desktop's bundled build when Update would move the host to it; absent from older desktops. */
availableUpdate?: string | null
}
export type OrcadManagedConversionResult =
| {
/** Committed on the server and retired from the SSH host. */
outcome: 'converted'
environment: PublicKnownRuntimeEnvironment
migrationId: string
/** Projects the server already held, so this desktop's own state for them did not move. */
linkedProjects?: SshManagedServerLinkedProject[]
/** Moved automations the server would not re-enable; they stay off there. */
automationsLeftDisabled?: number
}
| OrcadManagedDeferral
| OrcadManagedRefusal
export type OrcadManagedPendingMigrationRow = {
migrationId: string
environmentId: string
name: string
sshTargetId: string
phase: OrcadMigrationSourceCutoverPhase
startedAt: string
}
export type OrcadDeltaMoveRow = {
kind: 'repository' | 'folder-workspace' | 'project-group'
id: string
label: string
}
/** What moving a host's newer projects would add, and what the server will keep as it is. */
export type OrcadDeltaMovePreview = {
sshTargetId: string
environmentId: string
added: OrcadDeltaMoveRow[]
/** Projects an older build changed or removed; the server keeps its own copy of these. */
notReflected: { edited: OrcadDeltaMoveRow[]; removed: OrcadDeltaMoveRow[] }
blockers: OrcadMigrationBlocker[]
}
export type OrcadDeltaMoveResult =
| { outcome: 'moved'; migrationId: string }
| { outcome: 'refused'; code: string; reason: string; blockers?: OrcadMigrationBlocker[] }