fix: align environment delete cancellation guard

Split !impact.canDelete into two separate guards: a static blocker check
(managed_local, instance_default) that always rejects, and a runtime use check
that only rejects without force=true. On a forced delete that has an active
custom image session, cancel the session first; if cancellation leaves the
session in a non-terminal state (starting, waiting_for_user, capturing), return
409 instead of deleting the row. Then release active leases before removing the
environment.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Harold KimandPaperclip committed 2026-07-09 09:37:57 +00:00
1 parent 4da38087c8
commit c3bf218a1d
1 file changed
+34 -2
+34 -2
View File
@@ -12,6 +12,7 @@ import {
redactEnvironmentCustomImageTemplate,
startEnvironmentCustomImageSetupSessionSchema,
type EnvironmentDeleteBlastRadius,
type EnvironmentCustomImageSetupSessionStatus,
updateEnvironmentSchema,
} from "@paperclipai/shared";
import { conflict, forbidden, unprocessable } from "../errors.js";
@@ -47,9 +48,13 @@ import { listReadyPluginEnvironmentDrivers } from "../services/plugin-environmen
import { getConfiguredSecretProvider } from "../secrets/configured-provider.js";
import { assertBoardOrgAccess, getActorInfo } from "./authz.js";
import type { PluginWorkerManager } from "../services/plugin-worker-manager.js";
import { environmentService } from "../services/environments.js";
import { environmentService, ACTIVE_CUSTOM_IMAGE_SETUP_STATUSES } from "../services/environments.js";
import { executionWorkspaceService } from "../services/execution-workspaces.js";
function isActiveCustomImageSetupStatus(status: EnvironmentCustomImageSetupSessionStatus): boolean {
return (ACTIVE_CUSTOM_IMAGE_SETUP_STATUSES as readonly string[]).includes(status);
}
export function environmentRoutes(
db: Db,
options: { pluginWorkerManager?: PluginWorkerManager } = {},
@@ -813,10 +818,37 @@ export function environmentRoutes(
res.status(404).json({ error: "Environment not found" });
return;
}
if (!impact.canDelete) {
const hasStaticBlocker = impact.deleteBlockedReasons.some(
(r) => r === "managed_local" || r === "instance_default",
);
if (hasStaticBlocker) {
rejectEnvironmentDelete({ actor, environment: existing, impact });
}
const hasRuntimeBlocker = impact.deleteBlockedReasons.includes("active_runtime_use");
const forceDelete = req.query.force === "true";
if (hasRuntimeBlocker && !forceDelete) {
rejectEnvironmentDelete({ actor, environment: existing, impact });
}
if (hasRuntimeBlocker && forceDelete) {
const overview = await customImages.getOverview({ environmentId: existing.id });
if (overview.activeSession) {
const cancelled = await customImages.cancelSetupSession({
sessionId: overview.activeSession.id,
reason: "environment_force_delete",
});
if (isActiveCustomImageSetupStatus(cancelled.status)) {
throw conflict(
"Active custom image setup session could not be cancelled. Retry or wait for setup to complete before deleting.",
{ deleteBlockedReasons: ["active_runtime_use"] },
);
}
}
await svc.releaseActiveLeasesForEnvironment(existing.id);
}
const removed = await svc.removeIfDeletable(existing.id);
if (!removed) {
const latestImpact = await svc.getDeleteBlastRadius(existing.id);