Require revocable authorization for automatic lobby removal

This commit is contained in:
2026-10-06 23:06:04 -04:00
parent e012278bd9
commit e271f31325
18 changed files with 357 additions and 27 deletions
+15 -2
View File
@@ -1,4 +1,6 @@
import { assertScalingOwnership } from '../autoscaling/ownership.js';
import { removalAuthorization } from '../autoscaling/authorization.js';
import { scalingIntent } from '../autoscaling/admission.js';
import { putFileBlob, getFileBlob, blobSize } from '../db/file-blobs.js';
import { concurrencyBlocker, preparingJobs } from './concurrency.js';
import { isIP } from 'node:net';
@@ -18,7 +20,7 @@ import { prepareNativeLaunch, evaluateNativePlacement, type PreparedNativeLaunch
import { getServerKey, saveServerKey } from '../fleet/server-keys.js';
import { activeBetaRemovalBlocker, getServer } from '../fleet/servers.js';
import { nextLobbyNumber, type CreateServerInput, type CreateServerResult } from './instances.js';
import { beginDeployment, deploymentStage, finishDeployment, REMOVING_SERVER } from './history.js';
import { beginDeployment, deploymentStage, finishDeployment, REMOVING_SERVER, cancelRemoval } from './history.js';
import { recordProgress, type CheckInput } from './progress.js';
import { trackActivity } from '../pipeline/activity.js';
import { fleetSettings } from '../settings.js';
@@ -659,7 +661,11 @@ async function processJob(job: Job, eligibility: (job: Job) => string | null) {
JOIN watchdog_incidents i ON i.id=s.source_id JOIN server_deployments d ON d.id=s.operation_id
WHERE s.operation_id=? AND s.kind='watchdog' AND i.kind='health' AND d.triggered_by LIKE 'watchdog:%'`).get(job.id);
if (recoveredWatchdog && occupancy.players !== null) { resolveRecoveredHealthOperation(job.id, occupancy.players, occupancy.source); return false; }
if (occupancy.players !== 0) { hold(job, occupancy.players !== null ? `Waiting for ${occupancy.players} player(s) to leave.` : 'Waiting for a fresh local player count.'); return false; }
if (occupancy.players !== 0) {
if (job.action === 'destroy' && scalingIntent(job.id)) cancelRemoval(job.id, false);
else hold(job, occupancy.players !== null ? `Waiting for ${occupancy.players} player(s) to leave.` : 'Waiting for a fresh local player count.');
return false;
}
}
return true;
};
@@ -729,6 +735,13 @@ async function processJob(job: Job, eligibility: (job: Job) => string | null) {
? 'Host eligibility checked; player safety checks bypassed by admin override' : 'Host eligibility and safety checks passed' } });
const payload: Record<string, unknown> = files.length ? { stageId: job.id } : {};
if (overrideSafety) payload.safetyOverride = true;
if (job.action === 'destroy') {
const authorization = removalAuthorization(job.id, instance!.host_id);
if (authorization) {
if (overrideSafety) throw new Error('Autoscaling never bypasses player safety.');
payload.autoscaling = authorization;
}
}
if (prepared) {
deploymentStage(job.id, 'Preparing launch files for the host agent', { phase: 'prepare', recipeStatus: 'attempted',
checkpoint: { key: 'startup-queue', label: 'Startup capacity available' } });