mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-26 02:00:52 +01:00
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:
1 parent
4da38087c8
commit
c3bf218a1d
1 file changed
+34
-2
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user