mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 17:45:53 +01:00
fix: align environment delete cancellation guard
Rework the force-delete route guard to differentiate static blockers (`managed_local`, `instance_default`) from the runtime blocker (`active_runtime_use`). Static blockers always reject; the runtime blocker rejects unless `?force=true` is set. When `force=true`, attempt to cancel any active custom image setup session; if cancellation leaves the session in a non-terminal state (`starting`, `waiting_for_user`, `capturing`), return a 409 rather than deleting the row. Otherwise, release all active leases before proceeding to `removeIfDeletable`. Typed the active-status set as `const satisfies readonly EnvironmentCustomImageSetupSessionStatus[]` to ensure future status additions raise a compile error here. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
d1ca30dd56
commit
1baf833c3f
1 file changed
+39
-1
@@ -12,6 +12,7 @@ import {
|
||||
redactEnvironmentCustomImageTemplate,
|
||||
startEnvironmentCustomImageSetupSessionSchema,
|
||||
type EnvironmentDeleteBlastRadius,
|
||||
type EnvironmentCustomImageSetupSessionStatus,
|
||||
updateEnvironmentSchema,
|
||||
} from "@paperclipai/shared";
|
||||
import { conflict, forbidden, unprocessable } from "../errors.js";
|
||||
@@ -50,6 +51,16 @@ import type { PluginWorkerManager } from "../services/plugin-worker-manager.js";
|
||||
import { environmentService } from "../services/environments.js";
|
||||
import { executionWorkspaceService } from "../services/execution-workspaces.js";
|
||||
|
||||
const ACTIVE_CUSTOM_IMAGE_SETUP_STATUSES = [
|
||||
"starting",
|
||||
"waiting_for_user",
|
||||
"capturing",
|
||||
] as const satisfies readonly EnvironmentCustomImageSetupSessionStatus[];
|
||||
|
||||
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 +824,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