fix(cloud): EP-8 Delete works when this computer can't reach the sandbox (PR 1 of 2) (#683)

* fix(cloud): EP-8 Delete of a running sandbox this computer can't reach (revoked pairing) goes through Orca Cloud with Force delete, never waits silently

* fix(cloud): EP-8 refusal copy says sandbox and this computer (design-squad)
This commit is contained in:
Neil
2026-11-13 05:39:47 -08:00
committed by GitHub
parent 013354bdc5
commit e70193040f
9 changed files with 187 additions and 9 deletions
@@ -62,4 +62,40 @@ describe('preflightCloudWorkspaceDelete', () => {
gone: true
})
})
it("EP-8: a running sandbox whose server refuses or doesn't answer this computer is unreachable", async () => {
expect(
await preflightCloudWorkspaceDelete('sbx_1', answering('running'), async () => 'rejected')
).toEqual({ ok: true, unreachable: 'rejected' })
expect(
await preflightCloudWorkspaceDelete('sbx_1', answering('running'), async () => 'no_answer')
).toEqual({ ok: true, unreachable: 'no_answer' })
expect(
await preflightCloudWorkspaceDelete('sbx_1', answering('running'), async () => {
throw new Error('boom')
})
).toEqual({ ok: true, unreachable: 'no_answer' })
expect(
await preflightCloudWorkspaceDelete('sbx_1', answering('running'), async () => 'ok')
).toEqual({
ok: true
})
})
it('EP-8: never probes an asleep or destroyed sandbox', async () => {
let probed = 0
const probe = async (): Promise<'ok'> => {
probed += 1
return 'ok'
}
expect(await preflightCloudWorkspaceDelete('sbx_1', answering('suspended'), probe)).toEqual({
ok: true,
asleep: true
})
expect(await preflightCloudWorkspaceDelete('sbx_1', answering('destroyed'), probe)).toEqual({
ok: true,
gone: true
})
expect(probed).toBe(0)
})
})
@@ -8,11 +8,16 @@ import type { HostedSandboxControlPlaneClient } from './hosted-sandbox-control-p
/**
* `asleep`: no running server to remove its checkout; destroying the sandbox takes the disk.
* `gone`: Orca Cloud already destroyed it (or it never started); only the row is left.
* `unreachable` (EP-8): running, but this computer can't reach its server (its pairing was revoked,
* e.g. by an owner change, or it doesn't answer). Like asleep: Delete goes through Orca Cloud.
*/
export type CloudDeletePreflight =
| { ok: true; asleep?: boolean; gone?: boolean }
| { ok: true; asleep?: boolean; gone?: boolean; unreachable?: 'rejected' | 'no_answer' }
| ({ ok: false } & EphemeralVmFailure)
/** Can this computer reach the workspace's server right now? A short probe, never a wake. */
export type CloudServerProbe = () => Promise<'ok' | 'rejected' | 'no_answer'>
const ASLEEP_STATUSES = new Set(['suspending', 'suspended', 'archived'])
const GONE_STATUSES = new Set(['destroying', 'destroyed', 'failed'])
@@ -25,7 +30,8 @@ export function cloudSandboxIdForEnvironment(
export async function preflightCloudWorkspaceDelete(
sandboxId: string | undefined,
client: Pick<HostedSandboxControlPlaneClient, 'get'> | null
client: Pick<HostedSandboxControlPlaneClient, 'get'> | null,
probe?: CloudServerProbe
): Promise<CloudDeletePreflight> {
if (!sandboxId) {
// Not an Orca Cloud workspace (or its sandbox is already cleaned up): nothing to prove.
@@ -39,7 +45,12 @@ export async function preflightCloudWorkspaceDelete(
if (GONE_STATUSES.has(view.status)) {
return { ok: true, gone: true }
}
return ASLEEP_STATUSES.has(view.status) ? { ok: true, asleep: true } : { ok: true }
if (ASLEEP_STATUSES.has(view.status)) {
return { ok: true, asleep: true }
}
// Running on Orca Cloud: a Delete through a server this computer can't reach would wait forever.
const reach = probe ? await probe().catch(() => 'no_answer' as const) : 'ok'
return reach === 'ok' ? { ok: true } : { ok: true, unreachable: reach }
} catch (error) {
if (isEnvironmentProviderError(error, 'not_found')) {
return { ok: false, ...ephemeralVmFailure('cloudDeleteNotConfirmed') }
@@ -0,0 +1,30 @@
import { describe, expect, it } from 'vitest'
import { classifyProbeFailure, probeCloudServer } from './cloud-server-probe'
describe('probeCloudServer (EP-8)', () => {
it('tells a refused pairing apart from a server that does not answer', () => {
expect(classifyProbeFailure('unauthorized', '')).toBe('rejected')
expect(classifyProbeFailure('device_revoked', '')).toBe('rejected')
expect(classifyProbeFailure(undefined, 'Remote Orca runtime rejected the pairing token.')).toBe(
'rejected'
)
expect(classifyProbeFailure('timeout', 'Request timed out')).toBe('no_answer')
})
it('asks the server for its status, briefly', async () => {
const calls: unknown[][] = []
const ok = async (...args: unknown[]) => {
calls.push(args)
return { id: 'status.get', ok: true as const, result: {}, _meta: { runtimeId: 'r' } }
}
expect(await probeCloudServer('/data', 'env-1', ok)).toBe('ok')
expect(calls[0]?.slice(1, 5)).toEqual(['env-1', 'status.get', undefined, 5_000])
const refused = async () => {
throw Object.assign(new Error('Remote Orca runtime rejected the pairing token.'), {
code: 'unauthorized'
})
}
expect(await probeCloudServer('/data', 'env-1', refused)).toBe('rejected')
expect(await probeCloudServer('/data', '', ok)).toBe('no_answer')
})
})
@@ -0,0 +1,44 @@
// EP-8: can this computer reach an Orca Cloud workspace's server right now? A short status call over
// its saved pairing. A refused pairing (revoked by an owner change or a removal) is told apart from a
// server that just doesn't answer, so the caller can say which.
import { callRuntimeEnvironment } from '../ipc/runtime-environments/runtime-environment-transport-routing'
const PROBE_TIMEOUT_MS = 5_000
const REJECTED_CODES = new Set(['unauthorized', 'device_revoked', 'forbidden'])
export function classifyProbeFailure(
code: string | undefined,
message: string
): 'rejected' | 'no_answer' {
return (code && REJECTED_CODES.has(code)) || /rejected the pairing token/i.test(message)
? 'rejected'
: 'no_answer'
}
export async function probeCloudServer(
userDataPath: string,
environmentId: string,
call: typeof callRuntimeEnvironment = callRuntimeEnvironment
): Promise<'ok' | 'rejected' | 'no_answer'> {
if (!environmentId) {
return 'no_answer'
}
try {
const response = await call(
userDataPath,
environmentId,
'status.get',
undefined,
PROBE_TIMEOUT_MS
)
return response.ok
? 'ok'
: classifyProbeFailure(response.error?.code, response.error?.message ?? '')
} catch (error) {
const code =
error && typeof error === 'object' && 'code' in error && typeof error.code === 'string'
? error.code
: undefined
return classifyProbeFailure(code, error instanceof Error ? error.message : String(error))
}
}
@@ -28,6 +28,7 @@ import {
preflightCloudWorkspaceDelete,
type CloudDeletePreflight
} from '../hosted-sandbox/cloud-delete-preflight'
import { probeCloudServer } from '../hosted-sandbox/cloud-server-probe'
import { getHostedSandboxControlPlaneClient } from '../hosted-sandbox/hosted-sandbox-install'
import {
parsePendingVmCheckouts,
@@ -157,7 +158,8 @@ export function registerEphemeralVmRuntimeHandlers(store: Store): void {
app.getPath('userData'),
String(args?.runtimeEnvironmentId ?? '')
),
getHostedSandboxControlPlaneClient()
getHostedSandboxControlPlaneClient(),
() => probeCloudServer(app.getPath('userData'), String(args?.runtimeEnvironmentId ?? ''))
)
)
+1 -1
View File
@@ -132,7 +132,7 @@ export type EphemeralVmApi = {
preflightCloudDelete?: (args: {
runtimeEnvironmentId: string
}) => Promise<
| { ok: true; asleep?: boolean; gone?: boolean }
| { ok: true; asleep?: boolean; gone?: boolean; unreachable?: 'rejected' | 'no_answer' }
| { ok: false; error: string; errorCode?: string }
>
stopCleanup: (args: { runtimeId: string }) => Promise<EphemeralVmRuntimeRecord>
@@ -1,9 +1,15 @@
import { toast } from 'sonner'
import { translate } from '@/i18n/i18n'
import { ephemeralVmCleanupErrorText, ephemeralVmErrorText } from '@/runtime/ephemeral-vm/ephemeral-vm-error-copy'
import {
ephemeralVmCleanupErrorText,
ephemeralVmErrorText
} from '@/runtime/ephemeral-vm/ephemeral-vm-error-copy'
import type { EphemeralVmRuntimeRecord } from '../../../../../../shared/ephemeral-vm-runtimes'
import type { RuntimeClientTarget } from '../../../../../../shared/runtime-client-target'
import { CLOUD_ASLEEP_DELETE_REFUSAL } from '../../../../../../shared/worktree/removal'
import {
CLOUD_ASLEEP_DELETE_REFUSAL,
CLOUD_UNREACHABLE_DELETE_REFUSAL
} from '../../../../../../shared/worktree/removal'
import type { RemoveWorktreeOptions } from './worktree-removal-options'
import type { WorktreeSliceGet } from '../listing/worktree-slice-types'
import type { ExecutionHostId } from '../../../../../../shared/execution-host'
@@ -54,7 +60,10 @@ export async function cloudSandboxSkipsServer(
return false
}
const result = await preflightCloudDelete(runtimeEnvironmentId)?.catch(() => null)
return result?.ok === true && (result.asleep === true || result.gone === true)
return (
result?.ok === true &&
(result.asleep === true || result.gone === true || result.unreachable !== undefined)
)
}
/**
@@ -108,12 +117,18 @@ export async function prepareCloudSandboxDelete(args: {
if (!result.ok) {
throw new Error(ephemeralVmErrorText(result))
}
if (!result.asleep && !result.gone) {
// EP-8: running, but its server won't answer this computer (a revoked pairing): asking it would
// wait forever, so Delete goes through Orca Cloud like an asleep one, and says so.
const unreachable = result.unreachable !== undefined
if (!result.asleep && !result.gone && !unreachable) {
return args.options
}
if (result.asleep && !args.force) {
throw new Error(CLOUD_ASLEEP_DELETE_REFUSAL)
}
if (unreachable && !args.force) {
throw new Error(CLOUD_UNREACHABLE_DELETE_REFUSAL)
}
// forget-local skips the transport's own pre-send checks, so run them here.
args.assertBeforeDispatch()
return { ...args.options, mode: 'forget-local' }
@@ -122,4 +122,36 @@ describe('Orca Cloud workspace delete', () => {
expect(calledMethods()).not.toContain('worktree.rm')
expect(mockApi.worktrees.forgetLocal).toHaveBeenCalled()
})
it('EP-8: a running sandbox this computer cannot reach asks for Force delete, never waits on it', async () => {
const { store, wt } = seedCloudWorkspace()
Object.assign(mockApi.ephemeralVm, {
preflightCloudDelete: vi.fn().mockResolvedValue({ ok: true, unreachable: 'rejected' })
})
const result = await store.getState().removeWorktree({ id: wt.id, executionHostId: null })
expect(result.ok).toBe(false)
// Nothing went to the server whose pairing was refused (it would never answer).
expect(calledMethods()).toEqual([])
expect(mockApi.worktrees.forgetLocal).not.toHaveBeenCalled()
expect(store.getState().deleteStateByWorktreeId[wt.id]?.canForceDelete).toBe(true)
})
it('EP-8: Force delete of an unreachable sandbox destroys it through Orca Cloud', async () => {
const { store, wt } = seedCloudWorkspace()
Object.assign(mockApi.ephemeralVm, {
preflightCloudDelete: vi.fn().mockResolvedValue({ ok: true, unreachable: 'no_answer' })
})
mockApi.ephemeralVm.listRuntimes.mockImplementation(async () => [
{ id: 'runtime-cloud', workspaceId: wt.id, cleanupStatus: 'not_started' }
])
const result = await store.getState().removeWorktree({ id: wt.id, executionHostId: null }, true)
expect(result).toEqual({ ok: true })
expect(calledMethods()).toEqual([])
expect(mockApi.worktrees.forgetLocal).toHaveBeenCalled()
expect(mockApi.ephemeralVm.cleanup).toHaveBeenCalledWith({ runtimeId: 'runtime-cloud' })
})
})
+8
View File
@@ -121,6 +121,13 @@ const FORMATTED_DIRTY_WORKTREE_REMOVAL_PATTERN =
export const CLOUD_ASLEEP_DELETE_REFUSAL =
'This workspace is asleep, so Orca can’t check it for unsaved changes. Use Force delete to delete it and its disk without waking it.'
/**
* EP-8: a running Orca Cloud sandbox this computer can't reach (its pairing was revoked, or its server
* doesn't answer) can't be checked for unsaved work either; Force delete deletes it through Orca Cloud.
*/
export const CLOUD_UNREACHABLE_DELETE_REFUSAL =
'Orca can’t reach this workspace’s sandbox from this computer, so it can’t check it for unsaved changes. Use Force delete to delete it and its disk through Orca Cloud.'
export function classifyWorktreeForceDeleteReason(
error: string,
force = false,
@@ -154,6 +161,7 @@ export function classifyWorktreeForceDeleteReason(
}
if (
error === CLOUD_ASLEEP_DELETE_REFUSAL ||
error === CLOUD_UNREACHABLE_DELETE_REFUSAL ||
error.includes('Worktree has uncommitted or untracked changes') ||
error.includes('contains modified or untracked files') ||
FORMATTED_DIRTY_WORKTREE_REMOVAL_PATTERN.test(error)