diff --git a/config/scripts/locale-ko-key-overrides.json b/config/scripts/locale-ko-key-overrides.json index b722b98fc22e..8a86fb4bd74c 100644 --- a/config/scripts/locale-ko-key-overrides.json +++ b/config/scripts/locale-ko-key-overrides.json @@ -6264,7 +6264,7 @@ "ko": "이 호스트의 orca CLI가 Orca를 제어하도록 허용" }, "auto.components.settings.SshTargetForm.allowRemoteCliControlHelp": { - "ko": "켜면 이 호스트에서 실행한 orca 명령이 이 호스트의 작업 공간에서만, 연결되어 있는 동안에만 이 데스크톱의 Orca 브라우저 탭을 열고 조작할 수 있습니다. 이 호스트에서 이전 버전의 Orca가 시작한 터미널도 자신이 감독받는 작업에 대해 오케스트레이션 명령(check, inbox, send, ask, reply)을 실행할 수 있습니다. 파일, 클립보드, 쿠키는 읽을 수 없습니다. 끄면 이 호스트 자체의 Orca 서버에만 접근합니다." + "ko": "기본적으로 켜져 있습니다. 이 호스트에서 실행한 orca 명령이 이 호스트의 작업 공간에서만, 연결되어 있는 동안에만 이 데스크톱의 Orca 브라우저 탭을 열고 조작할 수 있게 하고, 이 호스트에서 이전 버전의 Orca가 시작한 터미널이 자신이 감독받는 작업에 대해 오케스트레이션 명령(check, inbox, send, ask, reply)을 실행할 수 있게 합니다. 파일, 클립보드, 쿠키는 읽을 수 없습니다. 끄면 이 호스트 자체의 Orca 서버에만 접근합니다." }, "auto.components.settings.AdvancedNetworkSettingsSection.3e431564b5": { "ko": "localhost, 127.0.0.1, *.internal" diff --git a/src/main/ipc/desktop-browser-bridge-wiring.test.ts b/src/main/ipc/desktop-browser-bridge-wiring.test.ts new file mode 100644 index 000000000000..58b0b8895178 --- /dev/null +++ b/src/main/ipc/desktop-browser-bridge-wiring.test.ts @@ -0,0 +1,71 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { SshConnectionState, SshTarget } from '../../shared/ssh-types' + +const { targets } = vi.hoisted(() => { + const targets: SshTarget[] = [] + return { targets } +}) + +vi.mock('electron', () => ({ webContents: { fromId: () => null } })) +vi.mock('../browser/browser-manager', () => ({ browserManager: {} })) +vi.mock('../runtime/rpc/methods', () => ({ ALL_RPC_METHODS: [] })) +vi.mock('./ssh-ipc-context', () => ({ persistedStore: null })) +vi.mock('../ssh/orcad-managed-tunnel', () => ({ ensureOrcadManagedTunnel: vi.fn() })) +vi.mock('../ssh/ssh-target-registry', async (importOriginal) => ({ + ...(await importOriginal>()), + getSshTargetRegistryStore: () => ({ listTargets: () => targets }) +})) +vi.mock('../../shared/managed-orcad-ssh-owner', async (importOriginal) => ({ + ...(await importOriginal>()), + getManagedOrcadFenceEnvironmentId: (target: SshTarget) => `env-${target.id}` +})) +vi.mock('./runtime-environment-connectivity-handlers', () => ({ + isRuntimeEnvironmentManuallyDisconnected: () => false +})) +vi.mock('../../shared/runtime-environment-store', async (importOriginal) => ({ + ...(await importOriginal>()), + resolveEnvironment: (_userDataPath: string, environmentId: string) => ({ environmentId }) +})) +vi.mock('../../shared/runtime-environments', async (importOriginal) => ({ + ...(await importOriginal>()), + getOrcadSshDeployment: ({ environmentId }: { environmentId: string }) => ({ + sshTargetId: environmentId.slice('env-'.length) + }) +})) + +import { listDesktopBrowserBridgeCandidates } from './desktop-browser-bridge-wiring' + +const host = (id: string, extra: Partial = {}): SshTarget => ({ + id, + label: id, + host: `${id}.example.com`, + port: 22, + username: 'me', + ...extra +}) +const connected = { + getState: (targetId: string): SshConnectionState => ({ + targetId, + status: 'connected', + error: null, + reconnectAttempt: 0 + }) +} + +beforeEach(() => { + targets.length = 0 +}) + +describe('which managed hosts may attach the desktop browser bridge', () => { + it('attaches a host with no saved switch or an explicit true, never an explicit false', () => { + targets.push( + host('absent'), + host('on', { allowRemoteCliControl: true }), + host('off', { allowRemoteCliControl: false }) + ) + + const candidates = listDesktopBrowserBridgeCandidates('/user-data', connected) + + expect(candidates.map((candidate) => candidate.targetId)).toEqual(['absent', 'on']) + }) +}) diff --git a/src/main/ipc/desktop-browser-bridge-wiring.ts b/src/main/ipc/desktop-browser-bridge-wiring.ts index a40483947162..a0c176c12eee 100644 --- a/src/main/ipc/desktop-browser-bridge-wiring.ts +++ b/src/main/ipc/desktop-browser-bridge-wiring.ts @@ -16,6 +16,7 @@ import { getPreferredPairedRuntimeConnection } from '../../shared/runtime-environments' import { getManagedOrcadFenceEnvironmentId } from '../../shared/managed-orcad-ssh-owner' +import { isRemoteCliControlAllowed } from '../../shared/ssh-remote-cli-control' import { browserManager } from '../browser/browser-manager' import { DesktopBrowserBridgeManager, @@ -49,27 +50,27 @@ function log(message: string): void { console.info(`[desktop-browser-bridge] ${message}`) } -function listDesktopBrowserBridgeCandidates( +export function listDesktopBrowserBridgeCandidates( userDataPath: string, connections: Pick | null ): DesktopBrowserBridgeCandidate[] { const candidates: DesktopBrowserBridgeCandidate[] = [] for (const target of getSshTargetRegistryStore()?.listTargets() ?? []) { - // Why strict true: the setting is opt-in, and an absent value is off. - if (target.allowRemoteCliControl !== true) { + if (!isRemoteCliControlAllowed(target)) { continue } if (connections?.getState(target.id)?.status !== 'connected') { continue } - // Why: an unrouted host's pages egress from this desktop, so it must not stay attached at all. - if (!isSshTargetBrowsingRouted(target.id)) { - continue - } + // Why first: with the switch on by default, every plain host reaches here; only managed ones qualify. const environmentId = getManagedOrcadFenceEnvironmentId(target) if (!environmentId || isRuntimeEnvironmentManuallyDisconnected(environmentId)) { continue } + // Why: an unrouted host's pages egress from this desktop, so it must not stay attached at all. + if (!isSshTargetBrowsingRouted(target.id)) { + continue + } try { const environment = resolveEnvironment(userDataPath, environmentId) if (getOrcadSshDeployment(environment)?.sshTargetId !== target.id) { diff --git a/src/main/persistence/leasing-ssh-ptys/ssh-normalization-remote-cli-consent.test.ts b/src/main/persistence/leasing-ssh-ptys/ssh-normalization-remote-cli-consent.test.ts deleted file mode 100644 index 97cfae686336..000000000000 --- a/src/main/persistence/leasing-ssh-ptys/ssh-normalization-remote-cli-consent.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { describe, expect, it } from 'vitest' -import type { SshTarget } from '../../../shared/ssh-types' -import { normalizeSshTarget } from './ssh-normalization' - -const base: SshTarget = { id: 'ssh-1', label: 'Box', host: 'box', port: 22, username: 'me' } - -describe("loading a host's orca CLI control switch (#29145)", () => { - it('turns off a switch saved before it let the host drive the desktop browser, and says why', () => { - const loaded = normalizeSshTarget({ ...base, allowRemoteCliControl: true }) - - expect(loaded.allowRemoteCliControl).toBeUndefined() - expect(loaded.allowRemoteCliControlReset).toBe(true) - }) - - it('keeps a switch the user turned on with its current meaning', () => { - const loaded = normalizeSshTarget({ - ...base, - allowRemoteCliControl: true, - allowRemoteCliControlConsent: 'desktop-browser-bridge' - }) - - expect(loaded.allowRemoteCliControl).toBe(true) - expect(loaded.allowRemoteCliControlReset).toBeUndefined() - }) - - it('keeps the notice across loads until the switch is saved, and drops a stray consent', () => { - const reset = normalizeSshTarget(normalizeSshTarget({ ...base, allowRemoteCliControl: true })) - expect(reset.allowRemoteCliControlReset).toBe(true) - - const stray = normalizeSshTarget({ - ...base, - allowRemoteCliControlConsent: 'desktop-browser-bridge' - }) - expect(stray.allowRemoteCliControlConsent).toBeUndefined() - expect(stray.allowRemoteCliControl).toBeUndefined() - }) -}) diff --git a/src/main/persistence/leasing-ssh-ptys/ssh-normalization-remote-cli-control.test.ts b/src/main/persistence/leasing-ssh-ptys/ssh-normalization-remote-cli-control.test.ts new file mode 100644 index 000000000000..30ac8e128af0 --- /dev/null +++ b/src/main/persistence/leasing-ssh-ptys/ssh-normalization-remote-cli-control.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from 'vitest' +import type { SshTarget } from '../../../shared/ssh-types' +import { isRemoteCliControlAllowed } from '../../../shared/ssh-remote-cli-control' +import { normalizeSshTarget } from './ssh-normalization' + +const base: SshTarget = { id: 'ssh-1', label: 'Box', host: 'box', port: 22, username: 'me' } + +describe("loading a host's orca CLI control switch", () => { + it('keeps a saved true on load, with or without the retired consent stamp', () => { + const plain = normalizeSshTarget({ ...base, allowRemoteCliControl: true }) + const stamped = normalizeSshTarget({ + ...base, + allowRemoteCliControl: true, + allowRemoteCliControlConsent: 'desktop-browser-bridge' + }) + + expect(plain.allowRemoteCliControl).toBe(true) + expect(isRemoteCliControlAllowed(plain)).toBe(true) + expect(stamped.allowRemoteCliControl).toBe(true) + }) + + it('keeps an explicit false off and an absent value on', () => { + const off = normalizeSshTarget({ ...base, allowRemoteCliControl: false }) + const absent = normalizeSshTarget(base) + + expect(off.allowRemoteCliControl).toBe(false) + expect(isRemoteCliControlAllowed(off)).toBe(false) + expect(absent.allowRemoteCliControl).toBeUndefined() + expect(isRemoteCliControlAllowed(absent)).toBe(true) + }) + + it('drops the retired reset notice, so a host an earlier build reset is on again', () => { + const resetByEarlierBuild = { ...base, allowRemoteCliControlReset: true } + const loaded = normalizeSshTarget(resetByEarlierBuild) + + expect(loaded).not.toHaveProperty('allowRemoteCliControlReset') + expect(isRemoteCliControlAllowed(loaded)).toBe(true) + }) + + it('never allows a missing target', () => { + expect(isRemoteCliControlAllowed(undefined)).toBe(false) + expect(isRemoteCliControlAllowed(null)).toBe(false) + }) +}) diff --git a/src/main/persistence/leasing-ssh-ptys/ssh-normalization.ts b/src/main/persistence/leasing-ssh-ptys/ssh-normalization.ts index aab631fa85c1..c3afcbeaaa29 100644 --- a/src/main/persistence/leasing-ssh-ptys/ssh-normalization.ts +++ b/src/main/persistence/leasing-ssh-ptys/ssh-normalization.ts @@ -17,6 +17,7 @@ export type LegacySshTarget = SshTarget & { remoteWorkspaceSyncEnabled?: unknown remoteWorkspaceSyncGracePeriodSeconds?: unknown experimentalPtySourceCreditV1?: unknown + allowRemoteCliControlReset?: unknown } // Why: old targets predate configHost; default to label-based lookup so imported SSH aliases still resolve via ssh -G. @@ -38,6 +39,8 @@ export function normalizeSshTarget(t: SshTarget): SshTarget { delete target.remoteRuntime delete target.remoteRuntimeResolution delete target.experimentalPtySourceCreditV1 + // Why: the one-time remote CLI control reset is retired; its notice flag means nothing now. + delete target.allowRemoteCliControlReset // Why: prefer the synced grace over stale relayGracePeriodSeconds so a user's "unlimited" (0) survives migration. const relayGracePeriodSeconds = legacySyncEnabled === true && typeof legacyGracePeriodSeconds === 'number' @@ -66,40 +69,19 @@ export function normalizeSshTarget(t: SshTarget): SshTarget { if (remoteRuntimeResolution) { normalized.remoteRuntimeResolution = remoteRuntimeResolution } - return normalizeRemoteCliControlConsent( - normalizeManagedServerLeaveBehind( - normalizeManagedServerMoveOffered( + return normalizeManagedServerLeaveBehind( + normalizeManagedServerMoveOffered( + normalizeAppVersionNote( normalizeAppVersionNote( - normalizeAppVersionNote( - migrateLegacyManagedOrcadOwner(normalized), - 'managedServerUnavailable' - ), - 'managedServerUpdateFailure' - ) + migrateLegacyManagedOrcadOwner(normalized), + 'managedServerUnavailable' + ), + 'managedServerUpdateFailure' ) ) ) } -/** - * The switch did nothing from 1.4.225 until the desktop browser bridge gave it a new meaning, so a - * `true` saved before that is not consent to it. Turn it off once and say why in settings. - */ -function normalizeRemoteCliControlConsent(target: SshTarget): SshTarget { - const { - allowRemoteCliControl, - allowRemoteCliControlConsent, - allowRemoteCliControlReset, - ...rest - } = target - if (allowRemoteCliControl === true) { - return allowRemoteCliControlConsent === 'desktop-browser-bridge' - ? { ...rest, allowRemoteCliControl, allowRemoteCliControlConsent } - : { ...rest, allowRemoteCliControlReset: true } - } - return allowRemoteCliControlReset === true ? { ...rest, allowRemoteCliControlReset } : rest -} - /** * Phase-3 builds fenced a managed host through `owner`, which shipped builds hide. Moving the fence * to `orcadFence` keeps the host visible, and reachable over its relay, after a downgrade. diff --git a/src/main/runtime/rpc/methods/ssh-target-management.test.ts b/src/main/runtime/rpc/methods/ssh-target-management.test.ts index a4989d9dc350..633b613d2420 100644 --- a/src/main/runtime/rpc/methods/ssh-target-management.test.ts +++ b/src/main/runtime/rpc/methods/ssh-target-management.test.ts @@ -172,7 +172,11 @@ describe('server SSH target management RPC', () => { const response = await dispatch('ssh.addTarget', { target: validInput }) - expect(mocks.mutations.add).toHaveBeenCalledWith({ ...validInput, source: 'manual' }) + expect(mocks.mutations.add).toHaveBeenCalledWith({ + ...validInput, + allowRemoteCliControl: false, + source: 'manual' + }) expect(response).toMatchObject({ ok: true, result: { @@ -198,6 +202,33 @@ describe('server SSH target management RPC', () => { expectNoSecrets(response) }) + it('reads an omitted CLI control switch from an older client as off, and lists it explicitly', async () => { + mocks.mutations.update.mockReturnValue({ ...storedTarget, ...validInput }) + await dispatch('ssh.updateTarget', { targetId: 'ssh-1', target: validInput }) + expect(mocks.mutations.update.mock.calls[0][1]).toMatchObject({ allowRemoteCliControl: false }) + + await dispatch('ssh.updateTarget', { + targetId: 'ssh-1', + target: { ...validInput, allowRemoteCliControl: true } + }) + expect(mocks.mutations.update.mock.calls[1][1]).toMatchObject({ allowRemoteCliControl: true }) + + mocks.listRegisteredSshTargets.mockReturnValue([ + { ...validInput, id: 'absent' }, + { ...validInput, id: 'off', allowRemoteCliControl: false } + ]) + const listed = await dispatch('ssh.listEditableTargets') + expect(listed).toMatchObject({ + ok: true, + result: { + targets: [ + { id: 'absent', allowRemoteCliControl: true }, + { id: 'off', allowRemoteCliControl: false } + ] + } + }) + }) + it('reports an unknown target instead of answering success', async () => { mocks.mutations.update.mockReturnValue(null) diff --git a/src/main/runtime/rpc/methods/ssh.ts b/src/main/runtime/rpc/methods/ssh.ts index 0f74474e3e84..ae459da092bb 100644 --- a/src/main/runtime/rpc/methods/ssh.ts +++ b/src/main/runtime/rpc/methods/ssh.ts @@ -11,6 +11,7 @@ import { browseSshDirectory } from '../../../ssh/ssh-remote-directory-browse' import { defineMethod } from '../core' import { getPublicSshError, getPublicSshState } from '../../public-ssh-state' import type { SshTarget as SshTargetRecord, SshTargetSummary } from '../../../../shared/ssh-types' +import { isRemoteCliControlAllowed } from '../../../../shared/ssh-remote-cli-control' import type { SshEditableTarget, SshEditableTargetField, @@ -64,9 +65,16 @@ function pickSshEditableFields(source: SshEditableTargetInput): SshEditableTarge } satisfies Record } +// Why: clients from before the default flip omit the switch when it shows off and read absent as +// off, so this boundary sends and saves it explicitly; current clients always send a boolean. +function withExplicitRemoteCliControl(input: SshEditableTargetInput): SshEditableTargetInput { + return { ...input, allowRemoteCliControl: input.allowRemoteCliControl ?? false } +} + function toSshEditableTarget(target: SshTargetRecord): SshEditableTarget { return { ...pickSshEditableFields(target), + allowRemoteCliControl: isRemoteCliControlAllowed(target), ...toSshTargetSummary(target), ...(target.source === undefined ? {} : { source: target.source }), ...(isManagedOrcadSshTarget(target) ? { managedServer: true as const } : {}) @@ -131,7 +139,10 @@ export const SSH_METHODS = [ permission: 'workspace', params: SshTargetAdd, handler: (params) => { - const result = getRegisteredSshTargetMutations().add({ ...params.target, source: 'manual' }) + const result = getRegisteredSshTargetMutations().add({ + ...withExplicitRemoteCliControl(params.target), + source: 'manual' + }) return { target: toSshEditableTarget(result.target), repoReadoptions: result.repoReadoptions @@ -145,7 +156,7 @@ export const SSH_METHODS = [ handler: (params) => { // Why: replace semantics, so an omitted optional field is cleared, as SSH settings do. const updated = getRegisteredSshTargetMutations().update(params.targetId, { - ...pickSshEditableFields(params.target), + ...pickSshEditableFields(withExplicitRemoteCliControl(params.target)), source: 'manual' }) if (!updated) { diff --git a/src/main/ssh/legacy-relay-bridge/legacy-relay-bridge-ipc.ts b/src/main/ssh/legacy-relay-bridge/legacy-relay-bridge-ipc.ts index 0462aaae4e34..620f74cde3ff 100644 --- a/src/main/ssh/legacy-relay-bridge/legacy-relay-bridge-ipc.ts +++ b/src/main/ssh/legacy-relay-bridge/legacy-relay-bridge-ipc.ts @@ -5,6 +5,7 @@ import { ipcMain, type BrowserWindow } from 'electron' import { z } from 'zod' import { LEGACY_RELAY_BRIDGE_CHANNELS as CHANNELS } from '../../../shared/legacy-relay-bridge-contract' +import { isRemoteCliControlAllowed } from '../../../shared/ssh-remote-cli-control' import type { Store } from '../../persistence' import type { OrcaRuntimeService } from '../../runtime/orca-runtime' import { track } from '../../telemetry/client' @@ -51,9 +52,8 @@ export function registerLegacyRelayBridgeIpc(args: { ? { remoteCli: { runtime: args.runtime, - // Why strict true: the setting is opt-in, and an absent value is off. allowed: (targetId: string) => - args.store.getSshTarget(targetId)?.allowRemoteCliControl === true + isRemoteCliControlAllowed(args.store.getSshTarget(targetId)) } } : {}), diff --git a/src/main/ssh/legacy-relay-bridge/legacy-relay-cli-call-limits.test.ts b/src/main/ssh/legacy-relay-bridge/legacy-relay-cli-call-limits.test.ts index 435a608eb791..533165635fc4 100644 --- a/src/main/ssh/legacy-relay-bridge/legacy-relay-cli-call-limits.test.ts +++ b/src/main/ssh/legacy-relay-bridge/legacy-relay-cli-call-limits.test.ts @@ -3,6 +3,7 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { toAppSshPtyId } from '../../../shared/ssh-pty-id' +import { isRemoteCliControlAllowed } from '../../../shared/ssh-remote-cli-control' import { OrcaRuntimeService } from '../../runtime/orca-runtime' import { OrchestrationDb } from '../../runtime/orchestration/db' import { @@ -65,13 +66,14 @@ const server = () => }) describe('limits on relay-era CLI calls', () => { - it('stops a waiting check once the per-host switch is turned off', async () => { + it('stops a waiting check once a default-on switch is turned off', async () => { + allowed = isRemoteCliControlAllowed({}) const waiting = server().cli( { argv: ['orchestration', 'check', '--wait', '--timeout-ms', '60000', '--json'], env: ENV }, new AbortController().signal ) await new Promise((resolve) => setTimeout(resolve, 50)) - allowed = false + allowed = isRemoteCliControlAllowed({ allowRemoteCliControl: false }) const started = Date.now() const result = await waiting expect(Date.now() - started).toBeLessThan(5_000) diff --git a/src/main/ssh/legacy-relay-bridge/legacy-relay-cli-server.test.ts b/src/main/ssh/legacy-relay-bridge/legacy-relay-cli-server.test.ts index 9911bc503ff6..f9a685deb7fc 100644 --- a/src/main/ssh/legacy-relay-bridge/legacy-relay-cli-server.test.ts +++ b/src/main/ssh/legacy-relay-bridge/legacy-relay-cli-server.test.ts @@ -3,6 +3,7 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { toAppSshPtyId } from '../../../shared/ssh-pty-id' +import { isRemoteCliControlAllowed } from '../../../shared/ssh-remote-cli-control' import { OrcaRuntimeService } from '../../runtime/orca-runtime' import { OrchestrationDb } from '../../runtime/orchestration/db' import { LEGACY_RELAY_CLI_UNVERIFIED } from './legacy-relay-cli-caller' @@ -138,6 +139,20 @@ describe('the relay-era orca CLI on the desktop runtime', () => { expect(db.getTask(ids.taskId)?.status).toBe('dispatched') }) + it.each([ + ['no saved value', {}, true], + ['an explicit true', { allowRemoteCliControl: true }, true], + ['an explicit false', { allowRemoteCliControl: false }, false] + ])('admits per the saved switch: %s', async (_label, saved, admitted) => { + allowed = isRemoteCliControlAllowed(saved) + const check = run(['orchestration', 'check', '--json']) + if (admitted) { + expect((await check).exitCode).toBe(0) + } else { + await expect(check).rejects.toThrow(LEGACY_RELAY_CLI_CONTROL_OFF) + } + }) + it('refuses commands outside orchestration', async () => { await expect(run(['worktree', 'list'])).rejects.toThrow(LEGACY_RELAY_CLI_UNSUPPORTED) await expect(run(['terminal', 'send', '--terminal', COORDINATOR])).rejects.toThrow( diff --git a/src/main/ssh/ssh-connection-store.test.ts b/src/main/ssh/ssh-connection-store.test.ts index d1dbb1e311aa..03b7dd54fc9e 100644 --- a/src/main/ssh/ssh-connection-store.test.ts +++ b/src/main/ssh/ssh-connection-store.test.ts @@ -58,7 +58,7 @@ describe('SshConnectionStore', () => { expect(mockStore.addSshTarget).toHaveBeenCalledWith(target) }) - it('records consent when the CLI control switch is saved on, and clears the reset notice (#29145)', () => { + it('saves the CLI control switch as given, with no consent stamp', () => { const target = sshStore.addTarget({ label: 'Box', host: 'box', @@ -66,17 +66,13 @@ describe('SshConnectionStore', () => { username: 'me', allowRemoteCliControl: true }) - expect(target.allowRemoteCliControlConsent).toBe('desktop-browser-bridge') + expect(target.allowRemoteCliControl).toBe(true) + expect(target).not.toHaveProperty('allowRemoteCliControlConsent') sshStore.updateTarget(target.id, { allowRemoteCliControl: false }) - expect(mockStore.updateSshTarget).toHaveBeenLastCalledWith( - target.id, - expect.objectContaining({ - allowRemoteCliControl: false, - allowRemoteCliControlConsent: undefined, - allowRemoteCliControlReset: undefined - }) - ) + expect(mockStore.updateSshTarget).toHaveBeenLastCalledWith(target.id, { + allowRemoteCliControl: false + }) sshStore.updateTarget(target.id, { label: 'Renamed' }) expect(mockStore.updateSshTarget).toHaveBeenLastCalledWith(target.id, { label: 'Renamed' }) }) diff --git a/src/main/ssh/ssh-connection-store.ts b/src/main/ssh/ssh-connection-store.ts index bb6e61c40abc..e2b6ba745506 100644 --- a/src/main/ssh/ssh-connection-store.ts +++ b/src/main/ssh/ssh-connection-store.ts @@ -37,7 +37,7 @@ export class SshConnectionStore { addTarget(target: Omit): SshTarget { const full: SshTarget = { - ...withRemoteCliControlConsent(target), + ...target, configHost: target.configHost ?? target.host, // Why: default to 'manual' so user-created targets are never overwritten // by a later ~/.ssh/config import (only 'ssh-config' targets are synced). @@ -102,10 +102,9 @@ export class SshConnectionStore { key in updates && existing?.[key] !== updates[key] const resetsRuntime = changed('remoteRuntime') || changed('host') || changed('port') || changed('configHost') - const consented = withRemoteCliControlConsent(updates) const updated = this.store.updateSshTarget( id, - resetsRuntime ? { ...consented, remoteRuntimeResolution: undefined } : consented + resetsRuntime ? { ...updates, remoteRuntimeResolution: undefined } : updates ) if (updated) { // Why: actively editing a target reclaims its alias from the deleted set, @@ -296,16 +295,3 @@ function isLegacyConfigImportTarget(target: SshTarget): boolean { alias && target.label === alias && target.configHost === alias && target.host !== alias ) } - -/** Saving the switch records consent to its current meaning and retires the reset notice. */ -function withRemoteCliControlConsent>(updates: T): T { - if (!('allowRemoteCliControl' in updates)) { - return updates - } - return { - ...updates, - allowRemoteCliControlConsent: - updates.allowRemoteCliControl === true ? 'desktop-browser-bridge' : undefined, - allowRemoteCliControlReset: undefined - } -} diff --git a/src/renderer/src/components/settings/SshHostAdvancedFields.tsx b/src/renderer/src/components/settings/SshHostAdvancedFields.tsx index cb8228331698..7fa856e88a2c 100644 --- a/src/renderer/src/components/settings/SshHostAdvancedFields.tsx +++ b/src/renderer/src/components/settings/SshHostAdvancedFields.tsx @@ -119,17 +119,9 @@ export function SshHostAdvancedFields({

{translate( 'auto.components.settings.SshTargetForm.allowRemoteCliControlHelp', - "When on, orca commands run on this host can open and drive Orca browser tabs on this desktop, only in this host's workspaces and only while it is connected. Terminals started by an older Orca on this host can also run orchestration commands (check, inbox, send, ask, reply) for their own supervised work. They cannot read your files, clipboard or cookies. When off, they reach only this host's own Orca server." + "On by default. Lets orca commands run on this host open and drive Orca browser tabs on this desktop, only in this host's workspaces and only while it is connected, and lets terminals started by an older Orca on this host run orchestration commands (check, inbox, send, ask, reply) for their own supervised work. They cannot read your files, clipboard or cookies. When off, they reach only this host's own Orca server." )}

- {form.allowRemoteCliControlReset && !form.allowRemoteCliControl ? ( -

- {translate( - 'auto.components.settings.SshTargetForm.allowRemoteCliControlReset', - 'Orca turned this off after an update, because it now lets this host drive your desktop browser and lets terminals started by an older Orca on this host run orchestration commands. Turn it on again only if you want that.' - )} -

- ) : null} 0 || form.jumpHost.trim().length > 0 || !form.systemSshConnectionReuse || - form.allowRemoteCliControl + !form.allowRemoteCliControl ) } diff --git a/src/renderer/src/components/settings/ssh-target-save-payload.test.ts b/src/renderer/src/components/settings/ssh-target-save-payload.test.ts index 70809995e9c9..296a54cd1b68 100644 --- a/src/renderer/src/components/settings/ssh-target-save-payload.test.ts +++ b/src/renderer/src/components/settings/ssh-target-save-payload.test.ts @@ -1,5 +1,10 @@ import { describe, expect, it } from 'vitest' -import { EMPTY_FORM, getEditingTargetForSshTarget, isSshTargetFormDirty } from './ssh-target-draft' +import { + EMPTY_FORM, + getEditingTargetForSshTarget, + hasAdvancedConnectionValues, + isSshTargetFormDirty +} from './ssh-target-draft' import { buildSshTargetSavePayload } from './ssh-target-save-payload' describe('buildSshTargetSavePayload', () => { @@ -12,20 +17,30 @@ describe('buildSshTargetSavePayload', () => { } }) - it('keeps remote CLI control off unless the user opts this host in, and clears it on update', () => { - const off = buildSshTargetSavePayload({ ...EMPTY_FORM, host: 'gpu.example.com' }) - const on = buildSshTargetSavePayload({ + it('saves remote CLI control on by default and an explicit false when turned off', () => { + const on = buildSshTargetSavePayload({ ...EMPTY_FORM, host: 'gpu.example.com' }) + const off = buildSshTargetSavePayload({ ...EMPTY_FORM, host: 'gpu.example.com', - allowRemoteCliControl: true + allowRemoteCliControl: false }) if (!off.ok || !on.ok) { throw new Error('payload rejected') } - expect(off.payload.target).not.toHaveProperty('allowRemoteCliControl') - expect(off.payload.updates).toHaveProperty('allowRemoteCliControl', undefined) expect(on.payload.target).toMatchObject({ allowRemoteCliControl: true }) expect(on.payload.updates).toMatchObject({ allowRemoteCliControl: true }) + expect(off.payload.target).toMatchObject({ allowRemoteCliControl: false }) + expect(off.payload.updates).toMatchObject({ allowRemoteCliControl: false }) + expect(hasAdvancedConnectionValues(EMPTY_FORM)).toBe(false) + expect(hasAdvancedConnectionValues({ ...EMPTY_FORM, allowRemoteCliControl: false })).toBe(true) + }) + + it('shows a host with no saved value as on, and an explicit false as off', () => { + const host = { id: 'gpu', label: 'gpu', host: 'gpu.example.com', port: 22, username: 'me' } + expect(getEditingTargetForSshTarget(host).allowRemoteCliControl).toBe(true) + expect( + getEditingTargetForSshTarget({ ...host, allowRemoteCliControl: false }).allowRemoteCliControl + ).toBe(false) }) it('round-trips the remote CLI control opt-in through the edit form', () => { diff --git a/src/renderer/src/components/settings/ssh-target-save-payload.ts b/src/renderer/src/components/settings/ssh-target-save-payload.ts index 7ad061ad7ff6..ce4fff12f1b5 100644 --- a/src/renderer/src/components/settings/ssh-target-save-payload.ts +++ b/src/renderer/src/components/settings/ssh-target-save-payload.ts @@ -49,7 +49,8 @@ export function buildSshTargetSavePayload(form: EditingTarget): SshTargetSavePay ...(proxyCommand ? { proxyCommand } : {}), ...(jumpHost ? { jumpHost } : {}), ...(systemSshConnectionReuse === false ? { systemSshConnectionReuse } : {}), - ...(form.allowRemoteCliControl ? { allowRemoteCliControl: true } : {}) + // Why explicit both ways: a server from before the default flip reads an absent value as off. + allowRemoteCliControl: form.allowRemoteCliControl } return { @@ -65,7 +66,6 @@ export function buildSshTargetSavePayload(form: EditingTarget): SshTargetSavePay proxyCommand, jumpHost, systemSshConnectionReuse, - allowRemoteCliControl: form.allowRemoteCliControl || undefined, source: 'manual' } } diff --git a/src/renderer/src/components/sidebar/add-remote-host-ssh-actions.test.ts b/src/renderer/src/components/sidebar/add-remote-host-ssh-actions.test.ts index 7b7042f3b0ea..d4e41dbf322d 100644 --- a/src/renderer/src/components/sidebar/add-remote-host-ssh-actions.test.ts +++ b/src/renderer/src/components/sidebar/add-remote-host-ssh-actions.test.ts @@ -115,6 +115,27 @@ describe('manual SSH host label fallback', () => { expect(outcome).toBe('saved') expect(savedTarget?.label).toBe('10.0.0.7') }) + + it.each([true, false])('saves the CLI control switch explicitly (%s)', async (allowed) => { + let savedTarget: SshTargetCreateInput | undefined + const ssh = { + listTargets: vi.fn().mockResolvedValue([]), + addTarget: vi.fn().mockImplementation(async ({ target }) => { + savedTarget = target + return { target: { ...target, id: 'ssh-1', source: 'manual' }, repoReadoptions: [] } + }) + } + + await saveNewSshHostFromForm({ + form: { ...EMPTY_FORM, host: '10.0.0.7', allowRemoteCliControl: allowed }, + ssh, + recordSshRepoReadoptions: vi.fn(), + setSshTargetsMetadata: vi.fn(), + recordFeatureInteraction: vi.fn() + }) + + expect(savedTarget).toHaveProperty('allowRemoteCliControl', allowed) + }) }) describe('bulk add of ~/.ssh/config hosts', () => { diff --git a/src/renderer/src/components/sidebar/add-remote-host-ssh-actions.ts b/src/renderer/src/components/sidebar/add-remote-host-ssh-actions.ts index 76ac27ab2f98..4af630a823cc 100644 --- a/src/renderer/src/components/sidebar/add-remote-host-ssh-actions.ts +++ b/src/renderer/src/components/sidebar/add-remote-host-ssh-actions.ts @@ -84,7 +84,9 @@ export async function saveNewSshHostFromForm({ ...(identityFile ? { identityFile } : {}), ...(proxyCommand ? { proxyCommand } : {}), ...(jumpHost ? { jumpHost } : {}), - ...(systemSshConnectionReuse === false ? { systemSshConnectionReuse } : {}) + ...(systemSshConnectionReuse === false ? { systemSshConnectionReuse } : {}), + // Why explicit both ways: absent means on here, and off on a server from before the default flip. + allowRemoteCliControl: form.allowRemoteCliControl } try { diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 8d492659482d..5a7ee75f089d 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -8996,8 +8996,7 @@ "8c922dffba": "Reuse SSH connection for faster setup", "53e9aabfc0": "Uses OpenSSH multiplexing when available. Turn off for hosts with custom SSH restrictions.", "allowRemoteCliControl": "Allow this host's orca CLI to control Orca", - "allowRemoteCliControlHelp": "When on, orca commands run on this host can open and drive Orca browser tabs on this desktop, only in this host's workspaces and only while it is connected. Terminals started by an older Orca on this host can also run orchestration commands (check, inbox, send, ask, reply) for their own supervised work. They cannot read your files, clipboard or cookies. When off, they reach only this host's own Orca server.", - "allowRemoteCliControlReset": "Orca turned this off after an update, because it now lets this host drive your desktop browser and lets terminals started by an older Orca on this host run orchestration commands. Turn it on again only if you want that.", + "allowRemoteCliControlHelp": "On by default. Lets orca commands run on this host open and drive Orca browser tabs on this desktop, only in this host's workspaces and only while it is connected, and lets terminals started by an older Orca on this host run orchestration commands (check, inbox, send, ask, reply) for their own supervised work. They cannot read your files, clipboard or cookies. When off, they reach only this host's own Orca server.", "editTitle": "Edit SSH host", "addTitle": "Add SSH host", "editDescription": "Update connection details for this machine. Changes apply on next connect.", diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index f40f86a0feeb..00b169293596 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -8719,8 +8719,7 @@ "addDescription": "Agrega una máquina persistente en la que puedas iniciar sesión por SSH.", "editingPrefix": "Editando", "allowRemoteCliControl": "Permitir que la orca CLI de este host controle Orca", - "allowRemoteCliControlHelp": "Cuando está activado, los comandos orca ejecutados en este host pueden abrir y controlar pestañas del navegador de Orca en este escritorio, solo en los espacios de trabajo de este host y solo mientras esté conectado. Las terminales iniciadas por una versión anterior de Orca en este host también pueden ejecutar comandos de orquestación (check, inbox, send, ask, reply) para su propio trabajo supervisado. No pueden leer tus archivos, tu portapapeles ni tus cookies. Cuando está desactivado, solo llegan al propio servidor de Orca de este host.", - "allowRemoteCliControlReset": "Orca lo desactivó tras una actualización, porque ahora permite que este host controle el navegador de tu escritorio y que las terminales iniciadas por una versión anterior de Orca en este host ejecuten comandos de orquestación. Vuelve a activarlo solo si lo deseas." + "allowRemoteCliControlHelp": "Activado de forma predeterminada. Permite que los comandos orca ejecutados en este host abran y controlen pestañas del navegador de Orca en este escritorio, solo en los espacios de trabajo de este host y solo mientras esté conectado, y que las terminales iniciadas por una versión anterior de Orca en este host ejecuten comandos de orquestación (check, inbox, send, ask, reply) para su propio trabajo supervisado. No pueden leer tus archivos, tu portapapeles ni tus cookies. Cuando está desactivado, solo llegan al propio servidor de Orca de este host." }, "TasksPane": { "f71d8a9dd3": "Proveedores de tareas", diff --git a/src/renderer/src/i18n/locales/fr.json b/src/renderer/src/i18n/locales/fr.json index 9aed99f8ef6d..92d5c61cc084 100644 --- a/src/renderer/src/i18n/locales/fr.json +++ b/src/renderer/src/i18n/locales/fr.json @@ -8869,8 +8869,7 @@ "addDescription": "Ajoutez une machine permanente sur laquelle vous pouvez vous connecter en SSH.", "editingPrefix": "Modification", "allowRemoteCliControl": "Autoriser l'orca CLI de cet hôte à contrôler Orca", - "allowRemoteCliControlHelp": "Lorsque cette option est activée, les commandes orca exécutées sur cet hôte peuvent ouvrir et piloter des onglets du navigateur Orca sur ce bureau, uniquement dans les espaces de travail de cet hôte et uniquement tant qu'il est connecté. Les terminaux lancés par une ancienne version d'Orca sur cet hôte peuvent aussi exécuter des commandes d'orchestration (check, inbox, send, ask, reply) pour leur propre travail supervisé. Elles ne peuvent pas lire vos fichiers, votre presse-papiers ni vos cookies. Lorsqu'elle est désactivée, elles n'atteignent que le serveur Orca de cet hôte.", - "allowRemoteCliControlReset": "Orca l'a désactivé après une mise à jour, car il permet désormais à cet hôte de piloter le navigateur de votre bureau et aux terminaux lancés par une ancienne version d'Orca sur cet hôte d'exécuter des commandes d'orchestration. Réactivez-le seulement si vous le souhaitez." + "allowRemoteCliControlHelp": "Activé par défaut. Permet aux commandes orca exécutées sur cet hôte d'ouvrir et de piloter des onglets du navigateur Orca sur ce bureau, uniquement dans les espaces de travail de cet hôte et uniquement tant qu'il est connecté, et aux terminaux lancés par une ancienne version d'Orca sur cet hôte d'exécuter des commandes d'orchestration (check, inbox, send, ask, reply) pour leur propre travail supervisé. Elles ne peuvent pas lire vos fichiers, votre presse-papiers ni vos cookies. Lorsque cette option est désactivée, elles n'atteignent que le serveur Orca de cet hôte." }, "TasksPane": { "f71d8a9dd3": "Fournisseurs de tâches", diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index 21378def8b4b..071548504fd4 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -8745,8 +8745,7 @@ "addDescription": "SSH でログインできる永続的なマシンを追加します。", "editingPrefix": "編集中", "allowRemoteCliControl": "このホストの orca CLI による Orca の操作を許可する", - "allowRemoteCliControlHelp": "オンにすると、このホストで実行される orca コマンドは、このホストのワークスペース内に限り、接続中のみ、このデスクトップの Orca ブラウザのタブを開いて操作できます。このホストで古い Orca が起動したターミナルも、自身の監督下の作業についてオーケストレーションコマンド(check、inbox、send、ask、reply)を実行できます。ファイル、クリップボード、Cookie は読み取れません。オフの場合、このホスト自身の Orca サーバーにのみアクセスします。", - "allowRemoteCliControlReset": "アップデート後に Orca がこれをオフにしました。この設定でこのホストがデスクトップのブラウザを操作でき、このホストで古い Orca が起動したターミナルがオーケストレーションコマンドを実行できるようになったためです。必要な場合のみ再度オンにしてください。" + "allowRemoteCliControlHelp": "デフォルトでオンです。このホストで実行される orca コマンドが、このホストのワークスペース内に限り、接続中のみ、このデスクトップの Orca ブラウザのタブを開いて操作できるようにし、このホストで古い Orca が起動したターミナルが自身の監督下の作業についてオーケストレーションコマンド(check、inbox、send、ask、reply)を実行できるようにします。ファイル、クリップボード、Cookie は読み取れません。オフの場合、このホスト自身の Orca サーバーにのみアクセスします。" }, "TasksPane": { "f71d8a9dd3": "タスクプロバイダー", diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index 9d08441a8d7c..cfe3e66b67cc 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -8708,8 +8708,7 @@ "addDescription": "SSH로 로그인할 수 있는 영구 머신을 추가합니다.", "editingPrefix": "편집 중", "allowRemoteCliControl": "이 호스트의 orca CLI가 Orca를 제어하도록 허용", - "allowRemoteCliControlHelp": "켜면 이 호스트에서 실행한 orca 명령이 이 호스트의 작업 공간에서만, 연결되어 있는 동안에만 이 데스크톱의 Orca 브라우저 탭을 열고 조작할 수 있습니다. 이 호스트에서 이전 버전의 Orca가 시작한 터미널도 자신이 감독받는 작업에 대해 오케스트레이션 명령(check, inbox, send, ask, reply)을 실행할 수 있습니다. 파일, 클립보드, 쿠키는 읽을 수 없습니다. 끄면 이 호스트 자체의 Orca 서버에만 접근합니다.", - "allowRemoteCliControlReset": "업데이트 후 Orca가 이 설정을 껐습니다. 이제 이 호스트가 데스크톱 브라우저를 조작할 수 있고, 이 호스트에서 이전 버전의 Orca가 시작한 터미널이 오케스트레이션 명령을 실행할 수 있게 되었기 때문입니다. 원하는 경우에만 다시 켜세요." + "allowRemoteCliControlHelp": "기본적으로 켜져 있습니다. 이 호스트에서 실행한 orca 명령이 이 호스트의 작업 공간에서만, 연결되어 있는 동안에만 이 데스크톱의 Orca 브라우저 탭을 열고 조작할 수 있게 하고, 이 호스트에서 이전 버전의 Orca가 시작한 터미널이 자신이 감독받는 작업에 대해 오케스트레이션 명령(check, inbox, send, ask, reply)을 실행할 수 있게 합니다. 파일, 클립보드, 쿠키는 읽을 수 없습니다. 끄면 이 호스트 자체의 Orca 서버에만 접근합니다." }, "TasksPane": { "f71d8a9dd3": "작업 제공자", diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 675c1509438a..0e3e01917f44 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -8708,8 +8708,7 @@ "addDescription": "添加一台可通过 SSH 登录的持久机器。", "editingPrefix": "正在编辑", "allowRemoteCliControl": "允许此主机的 orca CLI 控制 Orca", - "allowRemoteCliControlHelp": "开启后,在此主机上运行的 orca 命令可以打开并操作此桌面上的 Orca 浏览器标签页,但仅限此主机的工作区,且仅在主机连接期间。此主机上由旧版 Orca 启动的终端也可以为其自身受监督的工作运行编排命令(check、inbox、send、ask、reply)。它们无法读取你的文件、剪贴板或 Cookie。关闭时,它们只能访问此主机自身的 Orca 服务器。", - "allowRemoteCliControlReset": "更新后 Orca 关闭了此项,因为它现在允许此主机操作你桌面上的浏览器,并允许此主机上由旧版 Orca 启动的终端运行编排命令。仅在需要时再重新开启。" + "allowRemoteCliControlHelp": "默认开启。允许在此主机上运行的 orca 命令打开并操作此桌面上的 Orca 浏览器标签页,但仅限此主机的工作区,且仅在主机连接期间;并允许此主机上由旧版 Orca 启动的终端为其自身受监督的工作运行编排命令(check、inbox、send、ask、reply)。它们无法读取你的文件、剪贴板或 Cookie。关闭时,它们只能访问此主机自身的 Orca 服务器。" }, "TasksPane": { "f71d8a9dd3": "任务提供者", diff --git a/src/shared/ssh-remote-cli-control.ts b/src/shared/ssh-remote-cli-control.ts new file mode 100644 index 000000000000..6fe97312ee0b --- /dev/null +++ b/src/shared/ssh-remote-cli-control.ts @@ -0,0 +1,8 @@ +import type { SshTarget } from './ssh-types' + +/** On unless the user turned it off; a missing target (removed host) is never allowed. */ +export function isRemoteCliControlAllowed( + target: Pick | null | undefined +): boolean { + return target != null && target.allowRemoteCliControl !== false +} diff --git a/src/shared/ssh-types.ts b/src/shared/ssh-types.ts index 69ad46ba1f04..63b654dc4edb 100644 --- a/src/shared/ssh-types.ts +++ b/src/shared/ssh-types.ts @@ -71,13 +71,10 @@ export type SshTarget = { relayGracePeriodSeconds?: number /** Lets this host's `orca` CLI drive this desktop's Orca browser tabs in the host's own * workspaces (desktop browser bridge) and its relay-era terminals' orchestration commands. - * Off by default: the host is a guest, not an owner. */ + * On unless explicitly `false`; read it through `isRemoteCliControlAllowed`. */ allowRemoteCliControl?: boolean - /** Stamped when the switch is turned on with that meaning. A `true` without it was set while the - * switch did nothing (1.4.225 onward), so load turns it off and asks for a fresh opt-in. */ + /** Retired: written by earlier builds and ignored now. Kept so a downgrade still finds its stamp. */ allowRemoteCliControlConsent?: 'desktop-browser-bridge' - /** Orca turned the switch off for that reason; settings say why until the switch is saved again. */ - allowRemoteCliControlReset?: boolean /** Set to true after a successful connection that triggered a credential * prompt (passphrase or password). Persisted so startup reconnect can * partition targets into eager (no passphrase) vs deferred (passphrase) diff --git a/tests/e2e/ssh-orcad-desktop-browser-bridge.spec.ts b/tests/e2e/ssh-orcad-desktop-browser-bridge.spec.ts index 768fb990cad2..1571ec02cb95 100644 --- a/tests/e2e/ssh-orcad-desktop-browser-bridge.spec.ts +++ b/tests/e2e/ssh-orcad-desktop-browser-bridge.spec.ts @@ -109,7 +109,7 @@ function browserTabUrls(page: Page, worktreeId: string): Promise { }, worktreeId) } -test('a managed SSH host drives the desktop browser only while its CLI control setting is on', async (// oxlint-disable-next-line no-empty-pattern -- Owns app launch. +test('a managed SSH host drives the desktop browser by default until its CLI control setting is turned off', async (// oxlint-disable-next-line no-empty-pattern -- Owns app launch. {}, testInfo) => { test.setTimeout(8 * 60_000) const target = startDockerSshHostTarget(testInfo) @@ -146,13 +146,7 @@ test('a managed SSH host drives the desktop browser only while its CLI control s await activateManagedWorkspace(page, seeded.worktreeId, environmentId) const tabId = await openTerminalTab(page, seeded.worktreeId) - // Default off: refused with the setting's name, never "browser unavailable". - const refused = await hostCli(page, target, tabId, 'orca tab list') - expect(refused.ok).toBe(false) - expect(refused.error?.code).toBe('desktop_browser_bridge_unavailable') - expect(refused.error?.message).toContain(DESKTOP_BROWSER_BRIDGE_SETTING_LABEL) - - await setRemoteCliControl(page, seeded.targetId, true) + // No saved value: on by default. const createdUrl = `${SSH_REMOTE_ONLY_ORIGIN}/echo/bridge-created` const created = await hostCliUntilOk(page, target, tabId, `orca tab create --url ${createdUrl}`) const browserPageId = String(created.result?.browserPageId ?? '') @@ -206,7 +200,7 @@ test('a managed SSH host drives the desktop browser only while its CLI control s .poll(() => browserTabUrls(page, seeded.worktreeId), { timeout: 30_000 }) .not.toContain(navigatedUrl) - // Off again: the bridge detaches at once and the host is refused. + // Explicitly off: the bridge detaches at once and the host is refused with the setting's name. await setRemoteCliControl(page, seeded.targetId, false) await expect .poll(async () => (await hostCli(page, target, tabId, 'orca tab list')).error?.code ?? 'ok', { @@ -214,6 +208,8 @@ test('a managed SSH host drives the desktop browser only while its CLI control s intervals: [1_000] }) .toBe('desktop_browser_bridge_unavailable') + const refused = await hostCli(page, target, tabId, 'orca tab list') + expect(refused.error?.message).toContain(DESKTOP_BROWSER_BRIDGE_SETTING_LABEL) } finally { try { if (app) {