refactor(ssh): derive a host-key policy from the target owner (#30627)

* refactor(ssh): derive a host-key policy from the target owner

The verifier and decision took an isEphemeralRuntimeTarget boolean.
They now take a HostKeyPolicy derived from the owner by one function,
and the owner shape gets a named SshTargetOwner type. No behavior
change: on-demand runtime VMs map to the ephemeral policy, everything
else to standard, and every existing decision test passes unchanged in
substance.

* docs(ssh): separate owned-target hiding from the recipe-VM id prefix

Hidden-ness is decided by owner in main; the runtime-ssh- prefix also
carries per-provision semantics (fresh filesystem, VM cleanup), so only
on-demand-runtime owners may use it. Comments only.

* refactor(ssh): one id predicate for every Orca-owned SSH target

Surfaces that hide owned targets or must never dial them from the
renderer (startup re-dial, reconnect hints, automation host catalog,
status bar, host registry, state broadcast) now ask isOwnedSshTargetId,
which also covers the machine- prefix. isRuntimeOwnedSshTargetId keeps
its narrower recipe-VM meaning (address and filesystem replaced every
provision), so name retirement and VM cleanup are unchanged. No target
uses machine- on main yet, so behavior is identical; tests pin both.
This commit is contained in:
Neil
2026-10-19 01:54:22 -07:00
committed by GitHub
parent c18cb91dce
commit fa0bd69099
18 changed files with 141 additions and 46 deletions
+2 -2
View File
@@ -5,7 +5,7 @@ import type {
EnrichedDetectedPort,
SshConnectionState
} from '../../../shared/ssh-types'
import { isRuntimeOwnedSshTargetId } from '../../../shared/execution-host'
import { isOwnedSshTargetId } from '../../../shared/execution-host'
import {
enrichSshDetectedPorts,
enrichSshForwardEntries,
@@ -31,7 +31,7 @@ export function broadcastSshState(
): void {
const target = getSshTargetRegistryStore()?.getTarget(targetId)
// Why: owned targets are hidden from clients; broadcasting them leaks internal transport state into persisted reconnect hints.
if (isRuntimeOwnedSshTargetId(targetId) || (target && isRuntimeOwnedSshTarget(target))) {
if (isOwnedSshTargetId(targetId) || (target && isRuntimeOwnedSshTarget(target))) {
currentRuntime?.invalidateSshWorktreeScanCache?.(targetId)
return
}
@@ -265,7 +265,7 @@ export function getRuntimeOwnedSshTargetId(runtimeId: string): string {
return `${RUNTIME_OWNED_SSH_TARGET_ID_PREFIX}${runtimeId}`
}
/** Ephemeral runtime targets, hidden from SSH-host surfaces. */
/** Any target Orca created as an implementation detail; hidden from SSH-host surfaces. */
export function isRuntimeOwnedSshTarget(target: SshTarget): boolean {
return target.owner !== undefined
}
+6 -3
View File
@@ -57,7 +57,11 @@ import {
DEFAULT_SERVER_HOST_KEY_ALGORITHMS,
orderServerHostKeyAlgorithms
} from '../ssh-host-key-verifier'
import { HostKeyVerificationError, isHostKeyVerificationError } from '../ssh-host-key-decision'
import {
HostKeyVerificationError,
hostKeyPolicyForOwner,
isHostKeyVerificationError
} from '../ssh-host-key-decision'
import {
resolveWithSshG,
siteConfigMayRestrictHostKeys,
@@ -95,7 +99,6 @@ import type { FileUploadSession } from '../../providers/filesystem-provider-cont
import { openSshSessionChannelWithRetry, waitForSshChannelOpen } from '../ssh-channel-open'
import { tagSftpHandshakeCorruption } from '../sftp-handshake-corruption'
import { withTimeout } from '../../../shared/promise-timeout-fallback'
import { isEphemeralRuntimeSshOwner } from '../../../shared/managed-orcad-ssh-owner'
import { isSshUserDisconnected } from '../ssh-user-disconnect'
/** The sign-in a connection holds in memory, handed to the connection that replaces it. */
@@ -1230,7 +1233,7 @@ export class SshConnection {
hostKeyStoreFile: boundSshHostKeyStoreFile() ?? undefined,
strictHostKeyChecking: hostKeyResolved?.strictHostKeyChecking ?? 'ask',
isHostKeyAlias,
isEphemeralRuntimeTarget: isEphemeralRuntimeSshOwner(this.target.owner),
policy: hostKeyPolicyForOwner(this.target.owner),
siteConfigSuppressed,
// A file that EXISTS and will not open is the absence of evidence, not evidence of a new
// host — the entry that would have said "this key changed" may be in it. An ABSENT file is
+24 -8
View File
@@ -1,12 +1,16 @@
import { describe, expect, it } from 'vitest'
import { decideHostKey, type HostKeyDecisionInput } from './ssh-host-key-decision'
import {
decideHostKey,
hostKeyPolicyForOwner,
type HostKeyDecisionInput
} from './ssh-host-key-decision'
function input(overrides: Partial<HostKeyDecisionInput> = {}): HostKeyDecisionInput {
return {
knownHostsOutcome: 'unknown',
storeOutcome: 'unknown',
strictHostKeyChecking: 'ask',
isEphemeralRuntimeTarget: false,
policy: { kind: 'standard' },
siteConfigSuppressed: false,
knownHostsUnreadable: false,
displayHost: 'build-01',
@@ -311,12 +315,12 @@ describe('deciding what to do with a presented host key', () => {
// A fresh VM presents a new key every launch, so recording one would accumulate a row per
// launch and eventually turn a stale record into a spurious mismatch.
it('accepts an ephemeral runtime target without remembering it', () => {
expect(decideHostKey(input({ isEphemeralRuntimeTarget: true })).action).toBe('accept')
expect(decideHostKey(input({ policy: { kind: 'ephemeral' } })).action).toBe('accept')
})
it('still rejects a changed key for an ephemeral target', () => {
const decision = decideHostKey(
input({ isEphemeralRuntimeTarget: true, knownHostsOutcome: 'mismatch' })
input({ policy: { kind: 'ephemeral' }, knownHostsOutcome: 'mismatch' })
)
expect(decision.action).toBe('reject')
})
@@ -341,7 +345,7 @@ describe('deciding what to do with a presented host key', () => {
// runtimes off for everyone whose HOME diverges from their passwd home.
it('still accepts an ephemeral target when a source could not be read', () => {
const decision = decideHostKey(
input({ isEphemeralRuntimeTarget: true, siteConfigSuppressed: true })
input({ policy: { kind: 'ephemeral' }, siteConfigSuppressed: true })
)
expect(decision.action).toBe('accept')
})
@@ -350,7 +354,7 @@ describe('deciding what to do with a presented host key', () => {
// actually read and the user actually asked for, so it outranks the carve-out.
it('denies an ephemeral target under an explicit StrictHostKeyChecking=yes', () => {
const decision = decideHostKey(
input({ isEphemeralRuntimeTarget: true, strictHostKeyChecking: 'yes' })
input({ policy: { kind: 'ephemeral' }, strictHostKeyChecking: 'yes' })
)
expect(decision.action).toBe('reject')
})
@@ -359,7 +363,7 @@ describe('deciding what to do with a presented host key', () => {
it('still rejects a changed key for an ephemeral target with sources incomplete', () => {
const decision = decideHostKey(
input({
isEphemeralRuntimeTarget: true,
policy: { kind: 'ephemeral' },
siteConfigSuppressed: true,
storeOutcome: 'mismatch'
})
@@ -382,7 +386,7 @@ describe('deciding what to do with a presented host key', () => {
{ storeOutcome: 'mismatch' },
{ strictHostKeyChecking: 'yes' },
{ strictHostKeyChecking: 'no' },
{ isEphemeralRuntimeTarget: true },
{ policy: { kind: 'ephemeral' } },
{ siteConfigSuppressed: true },
{ knownHostsUnreadable: true }
]
@@ -391,3 +395,15 @@ describe('deciding what to do with a presented host key', () => {
}
})
})
describe('hostKeyPolicyForOwner', () => {
it('trusts first contact only for ephemeral runtime VMs', () => {
expect(hostKeyPolicyForOwner({ type: 'on-demand-runtime', runtimeId: 'vm-1' })).toEqual({
kind: 'ephemeral'
})
expect(
hostKeyPolicyForOwner({ type: 'on-demand-runtime', runtimeId: 'managed-orcad:env-1' })
).toEqual({ kind: 'standard' })
expect(hostKeyPolicyForOwner(undefined)).toEqual({ kind: 'standard' })
})
})
+18 -7
View File
@@ -5,6 +5,8 @@
* rather than importing its sources so a test states its own trust state instead of writing files.
* See docs/reference/ssh-host-key-verification.md.
*/
import { isEphemeralRuntimeSshOwner } from '../../shared/managed-orcad-ssh-owner'
import type { SshTargetOwner } from '../../shared/ssh-types'
import type { KnownHostsOutcome } from './config/ssh-known-hosts'
/** Phase 1 ships no dialog; `prompt` is reserved for Phase 2 and never produced today. */
@@ -19,6 +21,18 @@ export type HostKeyDecision = {
reason?: string
}
export type HostKeyPolicy =
| { kind: 'standard' }
/**
* A freshly provisioned VM presents a new key every launch, so first contact is expected rather
* than suspicious — and persisting would accumulate a record per launch.
*/
| { kind: 'ephemeral' }
export function hostKeyPolicyForOwner(owner: SshTargetOwner | undefined): HostKeyPolicy {
return isEphemeralRuntimeSshOwner(owner) ? { kind: 'ephemeral' } : { kind: 'standard' }
}
export type HostKeyDecisionInput = {
/** From the user's known_hosts files, unioned. */
knownHostsOutcome: KnownHostsOutcome
@@ -26,11 +40,8 @@ export type HostKeyDecisionInput = {
storeOutcome: 'match' | 'mismatch' | 'unknown-type-known-host' | 'unknown'
/** Effective `StrictHostKeyChecking`; anything unrecognised is treated as `ask`. */
strictHostKeyChecking: string
/**
* A freshly provisioned VM presents a new key every launch, so first contact is expected rather
* than suspicious — and persisting would accumulate a record per launch.
*/
isEphemeralRuntimeTarget: boolean
/** How this target's keys are trusted, decided by who owns the target. */
policy: HostKeyPolicy
/**
* `ssh -G` ran on the HOME-divergent `-F` path, which suppresses /etc/ssh/ssh_config, so a
* site-wide StrictHostKeyChecking may exist that we cannot see. We refuse a NEW host rather than
@@ -137,7 +148,7 @@ export function decideHostKey(input: HostKeyDecisionInput): HostKeyDecision {
const {
knownHostsOutcome,
storeOutcome,
isEphemeralRuntimeTarget,
policy,
siteConfigSuppressed,
knownHostsUnreadable,
displayHost,
@@ -255,7 +266,7 @@ export function decideHostKey(input: HostKeyDecisionInput): HostKeyDecision {
// Accepting without recording is the rest of it: a new key every launch would otherwise grow a
// row per VM, and a stale row eventually reads as a mismatch against a host that did nothing
// wrong.
if (isEphemeralRuntimeTarget) {
if (policy.kind === 'ephemeral') {
return { action: 'accept', outcome: unknownOutcome }
}
if (siteConfigSuppressed) {
+2 -2
View File
@@ -21,7 +21,7 @@ function deps(overrides: Partial<HostKeyVerifierDeps> = {}): HostKeyVerifierDeps
port: 22,
displayHost: 'example.com',
strictHostKeyChecking: 'ask',
isEphemeralRuntimeTarget: false,
policy: { kind: 'standard' },
siteConfigSuppressed: false,
knownHostsUnreadable: false,
entries: [],
@@ -121,7 +121,7 @@ describe('the ssh2 host key verifier', () => {
it.each([
['a key we already know', { entries: parseKnownHosts(`example.com ssh-ed25519 ${ED_A}`) }],
['a rejected key', { entries: parseKnownHosts(`example.com ssh-ed25519 ${ED_B}`) }],
['an ephemeral runtime target', { isEphemeralRuntimeTarget: true }],
['an ephemeral runtime target', { policy: { kind: 'ephemeral' as const } }],
['a lax StrictHostKeyChecking', { strictHostKeyChecking: 'no' }]
])('does not record %s', (_label, overrides) => {
const rememberHostKey = vi.fn()
+3 -3
View File
@@ -15,7 +15,7 @@ import {
type KnownHostsEntry,
type KnownHostsOutcome
} from './config/ssh-known-hosts'
import { decideHostKey, type HostKeyDecision } from './ssh-host-key-decision'
import { decideHostKey, type HostKeyDecision, type HostKeyPolicy } from './ssh-host-key-decision'
export type TrustedHostKeyLookup = (query: {
host: string
@@ -31,7 +31,7 @@ export type HostKeyVerifierDeps = {
/** True when `host` came from HostKeyAlias; ssh looks those up without the port. */
isHostKeyAlias?: boolean
strictHostKeyChecking: string
isEphemeralRuntimeTarget: boolean
policy: HostKeyPolicy
siteConfigSuppressed: boolean
knownHostsUnreadable: boolean
/** Already unioned across every known_hosts file. */
@@ -222,7 +222,7 @@ export function createHostKeyVerifier(
}),
storeOutcome: deps.isTrusted({ host: deps.host, port: deps.port, keyType, key }),
strictHostKeyChecking: deps.strictHostKeyChecking,
isEphemeralRuntimeTarget: deps.isEphemeralRuntimeTarget,
policy: deps.policy,
siteConfigSuppressed: deps.siteConfigSuppressed,
knownHostsUnreadable: deps.knownHostsUnreadable,
displayHost: deps.displayHost,
@@ -31,7 +31,7 @@ import {
} from '../components/terminal/terminal-provider-snapshot-capability'
import {
getRepoExecutionHostId,
isRuntimeOwnedSshTargetId,
isOwnedSshTargetId,
parseExecutionHostId,
toRuntimeExecutionHostId,
type ExecutionHostId
@@ -242,7 +242,7 @@ export function useAppStartupHydration(onOnboardingLoaded: (state: OnboardingSta
// Why: re-establish SSH before terminal reconnect so SSH-backed tabs route through pty.attach; passphrase targets defer to tab focus to avoid stacked credential dialogs.
// Why: never dial runtime-owned (ephemeral-VM) targets from the renderer — ssh.connect would dispose the runtime layer's live relay session; filter them out here too.
const connectionIds = (sessionRead.session.activeConnectionIdsAtShutdown ?? []).filter(
(targetId) => !isRuntimeOwnedSshTargetId(targetId)
(targetId) => !isOwnedSshTargetId(targetId)
)
if (connectionIds.length > 0) {
try {
@@ -4,7 +4,7 @@ import type {
StableAutomationAuthorityRef,
StableAutomationCatalogRef
} from '../../../../shared/automation-owner-ref'
import { isRuntimeOwnedSshTargetId } from '../../../../shared/execution-host'
import { isOwnedSshTargetId } from '../../../../shared/execution-host'
import {
automationAuthorityCatalogKey,
type AutomationCatalogSshMirrorInput,
@@ -164,7 +164,7 @@ function resolveSshScopeGap(
function projectSshEntries(ctx: AuthorityProjectionContext): AutomationHostCatalogEntry[] {
const liveTargets = new Map(
ctx.ssh.targets
.filter((target) => !isRuntimeOwnedSshTargetId(target.targetId))
.filter((target) => !isOwnedSshTargetId(target.targetId))
.map((target) => [target.targetId, target])
)
const candidateIds = new Set<string>(liveTargets.keys())
@@ -176,7 +176,7 @@ function projectSshEntries(ctx: AuthorityProjectionContext): AutomationHostCatal
}
const entries: AutomationHostCatalogEntry[] = []
for (const targetId of candidateIds) {
if (isRuntimeOwnedSshTargetId(targetId)) {
if (isOwnedSshTargetId(targetId)) {
continue
}
const live = liveTargets.get(targetId)
@@ -13,7 +13,7 @@ import type { SshConnectionStatus } from '../../../../shared/ssh-types'
import { translate } from '@/i18n/i18n'
import { getHostDisplayLabelOverrides } from '../../../../shared/host-setting-overrides'
import {
isRuntimeOwnedSshTargetId,
isOwnedSshTargetId,
toRuntimeExecutionHostId,
toSshExecutionHostId
} from '../../../../shared/execution-host'
@@ -79,7 +79,7 @@ export function SshStatusSegment({
const targets = Array.from(sshTargetLabels.entries())
// Why: runtime-owned (per-workspace-env) SSH targets are hidden — never list them
// as a user-facing SSH host in the status bar.
.filter(([id]) => !isRuntimeOwnedSshTargetId(id) && visibleHosts.has(toSshExecutionHostId(id)))
.filter(([id]) => !isOwnedSshTargetId(id) && visibleHosts.has(toSshExecutionHostId(id)))
.map(([id, label]) => {
const state = sshConnectionStates.get(id)
return {
@@ -0,0 +1,28 @@
import { describe, expect, it } from 'vitest'
import { toAppSshPtyId } from '../../../shared/ssh-pty-id'
import type { SshConnectionState } from '../../../shared/ssh-types'
import type { WorkspaceSessionSnapshot } from './workspace-session'
import { buildActiveConnectionIdsAtShutdown } from './workspace-session-reconnect-targets'
function connected(targetId: string): [string, SshConnectionState] {
return [targetId, { targetId, status: 'connected', error: null, reconnectAttempt: 0 }]
}
describe('buildActiveConnectionIdsAtShutdown', () => {
it('never asks startup to dial an Orca-owned target', () => {
const owned = ['runtime-ssh-vm-1', 'machine-0123']
const snapshot = {
sshConnectionStates: new Map([connected('ssh-user'), ...owned.map(connected)])
}
const remoteSessions = Object.fromEntries(
owned.map((id, index) => [`tab-${index}`, toAppSshPtyId(id, 'pty-1')])
)
expect(
buildActiveConnectionIdsAtShutdown(
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the builder reads only sshConnectionStates.
snapshot as WorkspaceSessionSnapshot,
remoteSessions
)
).toEqual(['ssh-user'])
})
})
@@ -1,6 +1,6 @@
import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types'
import { parseAppSshPtyId } from '../../../shared/ssh-pty-id'
import { isRuntimeOwnedSshTargetId } from '../../../shared/execution-host'
import { isOwnedSshTargetId } from '../../../shared/execution-host'
import type { WorkspaceSessionSnapshot } from './workspace-session'
export function buildActiveConnectionIdsAtShutdown(
@@ -16,9 +16,7 @@ export function buildActiveConnectionIdsAtShutdown(
// pane-level optimistic write could stamp one; exclude them here too.
const targetIds = new Set(
Array.from(snapshot.sshConnectionStates.entries())
.filter(
([targetId, state]) => state.status === 'connected' && !isRuntimeOwnedSshTargetId(targetId)
)
.filter(([targetId, state]) => state.status === 'connected' && !isOwnedSshTargetId(targetId))
.map(([targetId]) => targetId)
)
@@ -34,7 +32,7 @@ export function buildActiveConnectionIdsAtShutdown(
// renderer-driven ssh.connect would dispose the runtime's live relay session.
for (const sessionId of Object.values(remoteSessionIdsByTabId ?? {})) {
const connectionId = parseAppSshPtyId(sessionId)?.connectionId
if (!connectionId || isRuntimeOwnedSshTargetId(connectionId)) {
if (!connectionId || isOwnedSshTargetId(connectionId)) {
continue
}
const status = snapshot.sshConnectionStates.get(connectionId)?.status
@@ -92,6 +92,15 @@ describe('execution host registry', () => {
])
})
it('hides machine-provider SSH targets the same way', () => {
const hosts = buildExecutionHostRegistry({
repos: [{ connectionId: 'machine-0123', executionHostId: 'ssh:machine-0123' }],
settings: { activeRuntimeEnvironmentId: null },
sshTargetLabels: new Map([['machine-0123', 'omarchy-dev']])
})
expect(hosts.some((h) => h.id.includes('machine-0123'))).toBe(false)
})
it('hides runtime-owned (ephemeral VM) SSH targets from repo-derived hosts', () => {
const hosts = buildExecutionHostRegistry({
// A VM-backed repo carries the hidden runtime-owned target on both fields.
+4 -4
View File
@@ -5,7 +5,7 @@ import {
getRepoExecutionHostId,
getSettingsFocusedExecutionHostId,
getSshTargetIdForExecutionHost,
isRuntimeOwnedSshTargetId,
isOwnedSshTargetId,
normalizeHostPart,
parseExecutionHostId,
toRuntimeExecutionHostId,
@@ -259,14 +259,14 @@ export function buildExecutionHostRegistry(args: {
}
// Why: a VM-backed repo's executionHostId is `ssh:runtime-ssh-<id>`. Runtime-owned
// targets are hidden, so they must not become visible SSH run-target hosts here.
if (parsedHost?.kind === 'ssh' && !isRuntimeOwnedSshTargetId(parsedHost.targetId)) {
if (parsedHost?.kind === 'ssh' && !isOwnedSshTargetId(parsedHost.targetId)) {
sshTargetIds.add(parsedHost.targetId)
}
}
}
for (const targetId of args.sshTargetLabels?.keys() ?? []) {
const normalized = normalizeHostPart(targetId)
if (normalized && !isRuntimeOwnedSshTargetId(normalized)) {
if (normalized && !isOwnedSshTargetId(normalized)) {
sshTargetIds.add(normalized)
}
}
@@ -275,7 +275,7 @@ export function buildExecutionHostRegistry(args: {
// Why the host, not `connectionId`: a paired-server repo's `connectionId` is the server's
// own SSH target, which this client cannot dial.
const targetId = getSshTargetIdForExecutionHost(getRepoExecutionHostId(repo))
if (targetId && !isRuntimeOwnedSshTargetId(targetId)) {
if (targetId && !isOwnedSshTargetId(targetId)) {
sshTargetIds.add(targetId)
}
}
+13
View File
@@ -10,7 +10,9 @@ import {
getSettingsFocusedExecutionHostId,
getSshTargetIdForExecutionHost,
getWorktreeExecutionHostId,
isOwnedSshTargetId,
isRepoOnCatalogHost,
isRuntimeOwnedSshTargetId,
isUnresolvedOwnerHostId,
normalizeExecutionHostOrder,
normalizeExecutionHostScope,
@@ -218,3 +220,14 @@ describe('isRepoOnCatalogHost', () => {
expect(isRepoOnCatalogHost({ executionHostId: 'future:host' })).toBe(false)
})
})
describe('owned SSH target ids', () => {
it('hides recipe-VM and machine targets but keeps the recipe-VM meaning narrow', () => {
expect(isOwnedSshTargetId('runtime-ssh-vm-1')).toBe(true)
expect(isOwnedSshTargetId('machine-0123456789abcdef0123')).toBe(true)
expect(isOwnedSshTargetId('ssh-1792393612859-abc123')).toBe(false)
expect(isOwnedSshTargetId(null)).toBe(false)
// Only recipe VMs carry "replaced every provision" semantics.
expect(isRuntimeOwnedSshTargetId('machine-0123456789abcdef0123')).toBe(false)
})
})
+16
View File
@@ -77,12 +77,28 @@ export function toRuntimeExecutionHostId(environmentId: string): `runtime:${stri
// SSH/run-target surfaces. The renderer can't read the target.owner field, so it
// recognizes them by their deterministic id prefix. getRuntimeOwnedSshTargetId
// (main) builds on this same prefix to keep the two in sync.
// The prefix also means "address and filesystem are replaced every provision", so only
// on-demand-runtime owners may use it; main decides hidden-ness from `owner` alone.
export const RUNTIME_OWNED_SSH_TARGET_ID_PREFIX = 'runtime-ssh-'
/** Machine-provider targets: owned like recipe VMs, but persistent, so not `runtime-ssh-`. */
export const MACHINE_SSH_TARGET_ID_PREFIX = 'machine-'
/** A recipe VM's target, whose address and filesystem are replaced every provision. */
export function isRuntimeOwnedSshTargetId(targetId: string | null | undefined): boolean {
return typeof targetId === 'string' && targetId.startsWith(RUNTIME_OWNED_SSH_TARGET_ID_PREFIX)
}
/**
* Any target Orca owns as plumbing, by id because the renderer never sees `owner`: hidden from
* SSH-host surfaces and never dialed by the renderer, which would dispose its owner's session.
*/
export function isOwnedSshTargetId(targetId: string | null | undefined): boolean {
return (
isRuntimeOwnedSshTargetId(targetId) ||
(typeof targetId === 'string' && targetId.startsWith(MACHINE_SSH_TARGET_ID_PREFIX))
)
}
export function parseExecutionHostId(value: string | null | undefined): ParsedExecutionHost | null {
const normalized = normalizeHostPart(value)
if (!normalized) {
+1 -3
View File
@@ -1,10 +1,8 @@
import type { SshTarget } from './ssh-types'
import type { SshTarget, SshTargetOwner } from './ssh-types'
/** The runtime id phase-3 builds wrote into `owner` before the fence moved to `orcadFence`. */
const LEGACY_MANAGED_ORCAD_RUNTIME_ID_PREFIX = 'managed-orcad:'
type SshTargetOwner = NonNullable<SshTarget['owner']>
/** The managed Orca server this host serves, or null for a direct SSH host. */
export function getManagedOrcadFenceEnvironmentId(
target: Pick<SshTarget, 'orcadFence'> | undefined
+4 -1
View File
@@ -34,12 +34,15 @@ export type SshRemoteRuntimeResolution = {
orcaMajor: number
}
/** Who created a hidden target as an implementation detail, and so owns its lifecycle. */
export type SshTargetOwner = { type: 'on-demand-runtime'; runtimeId: string }
export type SshTarget = {
id: string
label: string
/** Internal owner for targets that Orca creates as implementation details.
* Owned targets are hidden from normal SSH-host management surfaces. */
owner?: { type: 'on-demand-runtime'; runtimeId: string }
owner?: SshTargetOwner
/** Host alias to resolve through OpenSSH config (ssh -G). */
configHost?: string
host: string