@@ -143,6 +143,7 @@ const codeViewerTicketTTLSeconds = 120;
143143const codeViewerSessionTTLSeconds = 8 * 60 * 60;
144144const egressTicketTTLSeconds = 120;
145145const runtimeAdapterTicketTTLSeconds = 120;
146+ const runtimeAdapterProvisionalClaimTTLSeconds = 10 * 60;
146147const runtimeAdapterDeleteRetryBaseMs = 5_000;
147148const runtimeAdapterDeleteRetryMaxMs = 60_000;
148149const runtimeAdapterDeleteDispatchGraceMs = 1_000;
@@ -257,6 +258,10 @@ interface RuntimeAdapterIdentityRecord {
257258 owner: string;
258259 org: string;
259260 createdAt: string;
261+ claimVersion?: 1;
262+ claimState?: "provisional" | "confirmed";
263+ claimExpiresAt?: string;
264+ confirmedAt?: string;
260265}
261266
262267interface RuntimeAdapterPendingRequest {
@@ -3675,6 +3680,7 @@ export class FleetCoordinator {
36753680 { status: 409 },
36763681 );
36773682 }
3683+ let runtimeAdapterIdentity: RuntimeAdapterIdentityRecord | undefined;
36783684 if (effectiveRuntimeAdapterID && effectiveRuntimeAdapterWorkspaceID) {
36793685 const identity = await this.state.storage.get<RuntimeAdapterIdentityRecord>(
36803686 runtimeAdapterIdentityKey(effectiveRuntimeAdapterID),
@@ -3701,6 +3707,7 @@ export class FleetCoordinator {
37013707 { status: 409 },
37023708 );
37033709 }
3710+ runtimeAdapterIdentity = identity;
37043711 }
37053712
37063713 const leases = await this.leaseRecords();
@@ -3817,6 +3824,9 @@ export class FleetCoordinator {
38173824 clearRuntimeAdapterDeleteMetadata(record);
38183825 }
38193826 await this.putLease(record);
3827+ if (runtimeAdapterIdentity) {
3828+ await this.confirmRuntimeAdapterIdentity(runtimeAdapterIdentity, nowISO);
3829+ }
38203830 await this.scheduleAlarm();
38213831 return json({ lease: record }, { status: existing ? 200 : 201 });
38223832 }
@@ -5802,28 +5812,49 @@ export class FleetCoordinator {
58025812 const owner = requestOwner(request);
58035813 const org = requestOrg(request, this.env);
58045814 const identityKey = runtimeAdapterIdentityKey(adapterID);
5805- const identity = await this.state.storage.get<RuntimeAdapterIdentityRecord>(identityKey);
5815+ await this.cleanupExpiredRuntimeAdapterTickets();
5816+ const now = new Date();
5817+ let identity = await this.state.storage.get<RuntimeAdapterIdentityRecord>(identityKey);
58065818 if (
58075819 identity &&
58085820 (identity.adapterID !== adapterID || identity.owner !== owner || identity.org !== org)
58095821 ) {
5810- return json(
5811- {
5812- error: "adapter_id_conflict",
5813- message: "runtime adapter id belongs to another owner or organization",
5814- },
5815- { status: 409 },
5816- );
5822+ if (!(await this.runtimeAdapterIdentityReclaimable(identity, adapterID, now.getTime()))) {
5823+ return json(
5824+ {
5825+ error: "adapter_id_conflict",
5826+ message: "runtime adapter id belongs to another owner or organization",
5827+ },
5828+ { status: 409 },
5829+ );
5830+ }
5831+ identity = undefined;
58175832 }
5818- await this.cleanupExpiredRuntimeAdapterTickets();
5819- const now = new Date();
58205833 if (!identity) {
5821- await this.state.storage.put<RuntimeAdapterIdentityRecord>(identityKey, {
5834+ identity = {
58225835 adapterID,
58235836 owner,
58245837 org,
58255838 createdAt: now.toISOString(),
5826- });
5839+ claimVersion: 1,
5840+ claimState: "provisional",
5841+ claimExpiresAt: new Date(
5842+ now.getTime() + runtimeAdapterProvisionalClaimTTLSeconds * 1000,
5843+ ).toISOString(),
5844+ };
5845+ await this.state.storage.put<RuntimeAdapterIdentityRecord>(identityKey, identity);
5846+ } else if (
5847+ identity.claimVersion === 1 &&
5848+ identity.claimState === "provisional" &&
5849+ !identity.confirmedAt
5850+ ) {
5851+ identity = {
5852+ ...identity,
5853+ claimExpiresAt: new Date(
5854+ now.getTime() + runtimeAdapterProvisionalClaimTTLSeconds * 1000,
5855+ ).toISOString(),
5856+ };
5857+ await this.state.storage.put<RuntimeAdapterIdentityRecord>(identityKey, identity);
58275858 }
58285859 const ticket: RuntimeAdapterTicketRecord = {
58295860 ticket: newRuntimeAdapterTicket(),
@@ -5877,6 +5908,7 @@ export class FleetCoordinator {
58775908 { status: 401 },
58785909 );
58795910 }
5911+ await this.confirmRuntimeAdapterIdentity(identity, new Date().toISOString());
58805912 const upgrade = this.state.createWebSocketUpgrade({
58815913 maxPayload: runtimeAdapterRelayFrameLimit,
58825914 });
@@ -7421,6 +7453,63 @@ export class FleetCoordinator {
74217453 );
74227454 }
74237455
7456+ private async runtimeAdapterIdentityReclaimable(
7457+ identity: RuntimeAdapterIdentityRecord,
7458+ adapterID: string,
7459+ now: number,
7460+ ): Promise<boolean> {
7461+ if (
7462+ identity.adapterID !== adapterID ||
7463+ identity.claimVersion !== 1 ||
7464+ identity.claimState !== "provisional" ||
7465+ identity.confirmedAt !== undefined ||
7466+ !Number.isFinite(Date.parse(identity.claimExpiresAt ?? "")) ||
7467+ Date.parse(identity.claimExpiresAt ?? "") > now ||
7468+ this.runtimeAdapterAgents.has(adapterID) ||
7469+ [...this.runtimeAdapterPending.values()].some((pending) => pending.adapterID === adapterID)
7470+ ) {
7471+ return false;
7472+ }
7473+ const [tickets, leases] = await Promise.all([
7474+ this.state.storage.list<RuntimeAdapterTicketRecord>({
7475+ prefix: runtimeAdapterTicketPrefix(),
7476+ }),
7477+ this.leaseRecords(),
7478+ ]);
7479+ if (
7480+ [...tickets.values()].some(
7481+ (ticket) => ticket.adapterID === adapterID && Date.parse(ticket.expiresAt) > now,
7482+ )
7483+ ) {
7484+ return false;
7485+ }
7486+ return !leases.some(
7487+ (lease) =>
7488+ lease.runtimeAdapterID === adapterID &&
7489+ (leaseIsLive(lease) || Boolean(lease.runtimeAdapterDeleteRequestedAt)),
7490+ );
7491+ }
7492+
7493+ private async confirmRuntimeAdapterIdentity(
7494+ identity: RuntimeAdapterIdentityRecord,
7495+ confirmedAt: string,
7496+ ): Promise<void> {
7497+ if (
7498+ identity.claimVersion !== 1 ||
7499+ identity.claimState !== "provisional" ||
7500+ identity.confirmedAt !== undefined
7501+ ) {
7502+ return;
7503+ }
7504+ const confirmed: RuntimeAdapterIdentityRecord = {
7505+ ...identity,
7506+ claimState: "confirmed",
7507+ confirmedAt,
7508+ };
7509+ delete confirmed.claimExpiresAt;
7510+ await this.state.storage.put(runtimeAdapterIdentityKey(identity.adapterID), confirmed);
7511+ }
7512+
74247513 private async consumeWebVNCTicket(
74257514 request: Request,
74267515 identifier: string,
0 commit comments