@opengeni/db 0.14.0 → 0.16.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/{chunk-ELMUCYZF.js → chunk-4RUK5CON.js} +183 -42
  2. package/dist/chunk-4RUK5CON.js.map +1 -0
  3. package/dist/chunk-HZD7KVAR.js +4648 -0
  4. package/dist/chunk-HZD7KVAR.js.map +1 -0
  5. package/dist/{chunk-Y5WZZVQK.js → chunk-SK7YYHBI.js} +5 -2
  6. package/dist/chunk-SK7YYHBI.js.map +1 -0
  7. package/dist/codex-token-resolver.d.ts +64 -0
  8. package/dist/connection-token-resolver.d.ts +90 -0
  9. package/dist/environment-crypto.d.ts +11 -0
  10. package/dist/event-payload-sanitizer.d.ts +32 -0
  11. package/dist/index.d.ts +6217 -10
  12. package/dist/index.js +6992 -2728
  13. package/dist/index.js.map +1 -1
  14. package/dist/insights.d.ts +157 -0
  15. package/dist/memory-domain.d.ts +44 -0
  16. package/dist/migrate.d.ts +5 -7
  17. package/dist/migrate.js +1 -1
  18. package/dist/new-session-drafts.d.ts +49 -0
  19. package/dist/persistence-errors.d.ts +69 -0
  20. package/dist/preference-registry-schema.d.ts +1147 -0
  21. package/dist/preference-registry.d.ts +878 -0
  22. package/dist/provision-roles.d.ts +4 -6478
  23. package/dist/provision-roles.js +1 -1
  24. package/dist/role-relationships.d.ts +35 -0
  25. package/dist/runtime-posture-cli.d.ts +1 -0
  26. package/dist/runtime-posture.d.ts +103 -0
  27. package/dist/schema.d.ts +23533 -3
  28. package/dist/schema.js +17 -1
  29. package/dist/session-control.d.ts +331 -0
  30. package/dist/session-queue-commands.d.ts +186 -0
  31. package/dist/session-tool-call-settlement.d.ts +32 -0
  32. package/dist/turn-initiator.d.ts +28 -0
  33. package/dist/workspace-instruction-policies-schema.d.ts +735 -0
  34. package/dist/workspace-instruction-policies.d.ts +64 -0
  35. package/drizzle/0136_unified_session_tool_policy.sql +109 -0
  36. package/drizzle/0137_preference_registry.sql +1347 -0
  37. package/drizzle/0138_sandbox_checkpoint_artifacts_and_deadlines.sql +1074 -0
  38. package/drizzle/0139_codex_provider_artifact_invalidations.sql +51 -0
  39. package/drizzle/0140_sandbox_restore_and_reaper_fences.sql +276 -0
  40. package/drizzle/0142_sandbox_archive_capture_gate.sql +80 -0
  41. package/drizzle/0143_session_codex_compaction_mode.sql +20 -0
  42. package/drizzle/0144_sandbox_viewer_force_drain_gate.sql +95 -0
  43. package/drizzle/0145_model_call_facts.sql +96 -0
  44. package/drizzle/0146_slack_bot_delete_idempotency.sql +105 -0
  45. package/drizzle/0147_draft_latency_mode.sql +12 -0
  46. package/drizzle/0148_session_turn_latency_mode.sql +12 -0
  47. package/package.json +6 -6
  48. package/src/index.ts +5095 -430
  49. package/src/insights.ts +837 -0
  50. package/src/migrate.ts +9 -1
  51. package/src/new-session-drafts.ts +4 -0
  52. package/src/preference-registry-schema.ts +170 -0
  53. package/src/preference-registry.ts +1220 -0
  54. package/src/provision-roles.ts +95 -22
  55. package/src/role-relationships.ts +132 -0
  56. package/src/runtime-posture.ts +28 -26
  57. package/src/schema.ts +484 -12
  58. package/src/session-queue-commands.ts +47 -46
  59. package/dist/chunk-ELMUCYZF.js.map +0 -1
  60. package/dist/chunk-PQKVUWQW.js +0 -4047
  61. package/dist/chunk-PQKVUWQW.js.map +0 -1
  62. package/dist/chunk-Y5WZZVQK.js.map +0 -1
  63. package/dist/schema-FoPub9yt.d.ts +0 -22666
@@ -0,0 +1,1074 @@
1
+ -- deployment-mode: maintenance
2
+ -- Make provider snapshot ownership and finite sandbox lifetime first-class
3
+ -- durable state. This is a one-way protocol cutover: old application workers
4
+ -- neither stamp provider deadlines nor honor rotation fences, so every
5
+ -- opengeni_app session must be stopped before this migration runs.
6
+
7
+ SET LOCAL lock_timeout = '5s';
8
+ SET LOCAL statement_timeout = '30min';
9
+
10
+ -- Reject an obviously live old application before waiting on table locks. The
11
+ -- guard runs again after the locks to close the connect/write race.
12
+ DO $maintenance_preflight_guard$
13
+ BEGIN
14
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app')
15
+ AND EXISTS (
16
+ SELECT 1
17
+ FROM pg_stat_activity
18
+ WHERE datname = current_database()
19
+ AND usename = 'opengeni_app'
20
+ AND pid <> pg_backend_pid()
21
+ )
22
+ THEN
23
+ RAISE EXCEPTION
24
+ 'sandbox checkpoint/deadline activation requires all opengeni_app sessions to be stopped'
25
+ USING ERRCODE = '55000';
26
+ END IF;
27
+ END
28
+ $maintenance_preflight_guard$;
29
+
30
+ -- Serialize the cutover against every old lease lifecycle writer. The migration
31
+ -- runner wraps this file in one transaction, so any timeout/guard failure leaves
32
+ -- the old schema and protocol fully active.
33
+ LOCK TABLE sandbox_leases IN ACCESS EXCLUSIVE MODE;
34
+ LOCK TABLE sandbox_lease_holders IN ACCESS EXCLUSIVE MODE;
35
+ LOCK TABLE sandbox_pty_sessions IN ACCESS EXCLUSIVE MODE;
36
+ LOCK TABLE sandbox_retained_processes IN ACCESS EXCLUSIVE MODE;
37
+ LOCK TABLE sandbox_workspace_mutation_admissions IN ACCESS EXCLUSIVE MODE;
38
+ LOCK TABLE session_turn_attempts IN SHARE MODE;
39
+ LOCK TABLE session_turns IN SHARE MODE;
40
+
41
+ DO $maintenance_guard$
42
+ BEGIN
43
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app')
44
+ AND EXISTS (
45
+ SELECT 1
46
+ FROM pg_stat_activity
47
+ WHERE datname = current_database()
48
+ AND usename = 'opengeni_app'
49
+ AND pid <> pg_backend_pid()
50
+ )
51
+ THEN
52
+ RAISE EXCEPTION
53
+ 'sandbox checkpoint/deadline activation requires all opengeni_app sessions to be stopped'
54
+ USING ERRCODE = '55000';
55
+ END IF;
56
+ END
57
+ $maintenance_guard$;
58
+
59
+ -- A provider establish call can outlive an abandoned Temporal activity. Older
60
+ -- workers kept touching the resulting holder from a private warmup timer even
61
+ -- after the exact attempt had closed, so TTL cleanup could never observe it as
62
+ -- stale. The maintenance fence makes this a race-free one-time reconciliation:
63
+ -- retain only canonical turn holders whose complete attempt/turn ownership
64
+ -- chain is still current, then restore the lease counters from source rows.
65
+ WITH removed AS MATERIALIZED (
66
+ DELETE FROM sandbox_lease_holders holder
67
+ WHERE holder.kind = 'turn'
68
+ AND holder.holder_id
69
+ ~* '^turn-attempt:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'
70
+ AND NOT EXISTS (
71
+ SELECT 1
72
+ FROM session_turn_attempts attempt
73
+ JOIN session_turns turn
74
+ ON turn.account_id = attempt.account_id
75
+ AND turn.workspace_id = attempt.workspace_id
76
+ AND turn.session_id = attempt.session_id
77
+ AND turn.id = attempt.turn_id
78
+ AND turn.execution_generation = attempt.execution_generation
79
+ AND turn.active_attempt_id = attempt.id
80
+ WHERE holder.holder_id = ('turn-attempt:' || attempt.id::text)
81
+ AND attempt.account_id = holder.account_id
82
+ AND attempt.workspace_id = holder.workspace_id
83
+ AND attempt.session_id = holder.subject_id
84
+ AND attempt.state IN ('claimed', 'running')
85
+ )
86
+ RETURNING holder.id AS holder_id, holder.lease_id
87
+ ),
88
+ affected AS MATERIALIZED (
89
+ SELECT DISTINCT removed.lease_id AS id FROM removed
90
+ ),
91
+ -- Data-modifying CTE siblings share one base-table snapshot in PostgreSQL.
92
+ -- The physical DELETE is therefore not visible to these counts; subtract its
93
+ -- exact RETURNING identities explicitly instead of silently retaining stale
94
+ -- denormalized counters.
95
+ counts AS MATERIALIZED (
96
+ SELECT affected.id,
97
+ (SELECT count(*) FROM sandbox_lease_holders holder
98
+ WHERE holder.lease_id = affected.id
99
+ AND NOT EXISTS (
100
+ SELECT 1 FROM removed WHERE removed.holder_id = holder.id
101
+ ))::integer AS total,
102
+ (SELECT count(*) FROM sandbox_lease_holders holder
103
+ WHERE holder.lease_id = affected.id
104
+ AND holder.kind = 'turn'
105
+ AND NOT EXISTS (
106
+ SELECT 1 FROM removed WHERE removed.holder_id = holder.id
107
+ ))::integer AS turns,
108
+ (SELECT count(*) FROM sandbox_lease_holders holder
109
+ WHERE holder.lease_id = affected.id
110
+ AND holder.kind = 'viewer'
111
+ AND NOT EXISTS (
112
+ SELECT 1 FROM removed WHERE removed.holder_id = holder.id
113
+ ))::integer AS viewers
114
+ FROM affected
115
+ )
116
+ UPDATE sandbox_leases lease SET
117
+ refcount = counts.total,
118
+ turn_holders = counts.turns,
119
+ viewer_holders = counts.viewers,
120
+ liveness = CASE
121
+ WHEN lease.liveness = 'warm' AND counts.total = 0 THEN 'draining'
122
+ ELSE lease.liveness
123
+ END,
124
+ expires_at = CASE
125
+ WHEN lease.liveness = 'warm' AND counts.total = 0
126
+ THEN now() - interval '1 millisecond'
127
+ ELSE lease.expires_at
128
+ END,
129
+ updated_at = now()
130
+ FROM counts
131
+ WHERE lease.id = counts.id;
132
+
133
+ -- A provider instance may disappear after its lease row has already advanced
134
+ -- to a successor identity. The reconciliation worker can still prove the exact
135
+ -- historical Modal sandbox is terminal by id; persist that distinct proof
136
+ -- instead of retrying an impossible current-lease identity match forever.
137
+ ALTER TABLE sandbox_retained_processes
138
+ ADD COLUMN provider_binding_key text,
139
+ ADD COLUMN provider_binding jsonb,
140
+ DROP CONSTRAINT sandbox_retained_processes_reconcile_proof_check,
141
+ ADD CONSTRAINT sandbox_retained_processes_provider_binding_check
142
+ CHECK (
143
+ (
144
+ provider_binding_key IS NULL
145
+ AND provider_binding IS NULL
146
+ ) OR (
147
+ provider_backend = 'modal'
148
+ AND octet_length(provider_binding_key) BETWEEN 1 AND 1024
149
+ AND jsonb_typeof(provider_binding) = 'object'
150
+ AND provider_binding_key::jsonb = provider_binding
151
+ AND provider_binding_key = format(
152
+ '{"version":1,"serverUrl":%s,"workspaceName":%s,"environment":%s}',
153
+ to_jsonb(provider_binding ->> 'serverUrl')::text,
154
+ to_jsonb(provider_binding ->> 'workspaceName')::text,
155
+ to_jsonb(provider_binding ->> 'environment')::text
156
+ )
157
+ AND provider_binding = jsonb_build_object(
158
+ 'version', 1,
159
+ 'serverUrl', provider_binding ->> 'serverUrl',
160
+ 'workspaceName', provider_binding ->> 'workspaceName',
161
+ 'environment', provider_binding ->> 'environment'
162
+ )
163
+ AND coalesce(octet_length(provider_binding ->> 'serverUrl'), 0) > 0
164
+ AND coalesce(octet_length(provider_binding ->> 'workspaceName'), 0) > 0
165
+ AND provider_binding ->> 'environment' IS NOT NULL
166
+ )
167
+ ),
168
+ ADD CONSTRAINT sandbox_retained_processes_reconcile_proof_check
169
+ CHECK (
170
+ (
171
+ reconcile_proof_outcome IS NULL
172
+ AND reconcile_proof_exit_code IS NULL
173
+ AND reconcile_proof_reason IS NULL
174
+ AND reconcile_proof_observed_at IS NULL
175
+ ) OR (
176
+ reconcile_proof_outcome = 'exited'
177
+ AND reconcile_proof_exit_code IS NOT NULL
178
+ AND reconcile_proof_reason = 'provider_exit_banner'
179
+ AND reconcile_proof_observed_at IS NOT NULL
180
+ ) OR (
181
+ reconcile_proof_outcome = 'lost'
182
+ AND reconcile_proof_exit_code IS NULL
183
+ AND reconcile_proof_reason IN (
184
+ 'provider_session_lost_banner',
185
+ 'provider_instance_not_found',
186
+ 'provider_instance_terminated'
187
+ )
188
+ AND reconcile_proof_observed_at IS NOT NULL
189
+ )
190
+ );
191
+
192
+ CREATE TABLE sandbox_checkpoint_artifacts (
193
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
194
+ account_id uuid NOT NULL,
195
+ workspace_id uuid NOT NULL,
196
+ sandbox_group_id uuid NOT NULL,
197
+ source_lease_id uuid NOT NULL,
198
+ source_lease_epoch integer NOT NULL,
199
+ source_instance_id text,
200
+ source_workspace_generation integer,
201
+ provenance text NOT NULL,
202
+ provider_backend text NOT NULL,
203
+ provider_binding_key text NOT NULL,
204
+ provider_binding jsonb NOT NULL,
205
+ object_kind text NOT NULL,
206
+ object_id text NOT NULL,
207
+ archive_base64 text NOT NULL,
208
+ archive_sha256 text NOT NULL,
209
+ archive_bytes integer NOT NULL,
210
+ descriptor jsonb NOT NULL,
211
+ descriptor_revision text NOT NULL,
212
+ state text NOT NULL DEFAULT 'candidate',
213
+ delete_after timestamptz,
214
+ delete_attempts integer NOT NULL DEFAULT 0,
215
+ delete_claim_id uuid,
216
+ delete_claimed_at timestamptz,
217
+ last_delete_error text,
218
+ created_at timestamptz NOT NULL DEFAULT now(),
219
+ published_at timestamptz,
220
+ deleted_at timestamptz,
221
+ updated_at timestamptz NOT NULL DEFAULT now(),
222
+
223
+ CONSTRAINT sandbox_checkpoint_artifacts_source_check CHECK (
224
+ source_lease_epoch >= 0
225
+ AND (
226
+ (
227
+ provenance = 'native_capture'
228
+ AND source_workspace_generation IS NOT NULL
229
+ AND source_workspace_generation >= 0
230
+ AND source_instance_id IS NOT NULL
231
+ AND octet_length(source_instance_id) BETWEEN 1 AND 512
232
+ )
233
+ OR (
234
+ provenance = 'legacy_provider_adopted'
235
+ AND source_workspace_generation IS NULL
236
+ AND source_instance_id IS NULL
237
+ )
238
+ )
239
+ ),
240
+ CONSTRAINT sandbox_checkpoint_artifacts_provider_check CHECK (
241
+ provider_backend = 'modal'
242
+ AND octet_length(provider_binding_key) BETWEEN 1 AND 1024
243
+ AND jsonb_typeof(provider_binding) = 'object'
244
+ AND provider_binding_key::jsonb = provider_binding
245
+ AND provider_binding_key = format(
246
+ '{"version":1,"serverUrl":%s,"workspaceName":%s,"environment":%s}',
247
+ to_jsonb(provider_binding ->> 'serverUrl')::text,
248
+ to_jsonb(provider_binding ->> 'workspaceName')::text,
249
+ to_jsonb(provider_binding ->> 'environment')::text
250
+ )
251
+ AND provider_binding = jsonb_build_object(
252
+ 'version', 1,
253
+ 'serverUrl', provider_binding ->> 'serverUrl',
254
+ 'workspaceName', provider_binding ->> 'workspaceName',
255
+ 'environment', provider_binding ->> 'environment'
256
+ )
257
+ AND coalesce(octet_length(provider_binding ->> 'serverUrl'), 0) > 0
258
+ AND coalesce(octet_length(provider_binding ->> 'workspaceName'), 0) > 0
259
+ AND provider_binding ->> 'environment' IS NOT NULL
260
+ AND object_kind IN ('modal_filesystem_snapshot', 'modal_directory_snapshot')
261
+ AND octet_length(object_id) BETWEEN 1 AND 1024
262
+ ),
263
+ CONSTRAINT sandbox_checkpoint_artifacts_archive_check CHECK (
264
+ archive_bytes > 0
265
+ AND octet_length(archive_base64) > 0
266
+ AND archive_sha256 ~ '^[0-9a-f]{64}$'
267
+ AND jsonb_typeof(descriptor) = 'object'
268
+ AND octet_length(descriptor_revision) BETWEEN 1 AND 256
269
+ AND translate(encode(decode(archive_base64, 'base64'), 'base64'), E'\n\r', '')
270
+ = archive_base64
271
+ AND octet_length(decode(archive_base64, 'base64')) = archive_bytes
272
+ AND encode(digest(decode(archive_base64, 'base64'), 'sha256'), 'hex')
273
+ = archive_sha256
274
+ AND descriptor ->> 'version' = '2'
275
+ AND descriptor ->> 'kind' = 'provider_snapshot'
276
+ AND descriptor ->> 'revision' = descriptor_revision
277
+ AND descriptor ->> 'archiveSha256' = archive_sha256
278
+ AND CASE
279
+ WHEN descriptor ->> 'archiveBytes' ~ '^[1-9][0-9]*$'
280
+ THEN (descriptor ->> 'archiveBytes')::bigint = archive_bytes
281
+ ELSE false
282
+ END
283
+ AND descriptor ->> 'snapshotId' = object_id
284
+ AND (
285
+ (
286
+ object_kind = 'modal_filesystem_snapshot'
287
+ AND descriptor ->> 'provider' = 'modal_snapshot_filesystem'
288
+ AND descriptor ->> 'workspacePersistence' = 'snapshot_filesystem'
289
+ )
290
+ OR (
291
+ object_kind = 'modal_directory_snapshot'
292
+ AND descriptor ->> 'provider' = 'modal_snapshot_directory'
293
+ AND descriptor ->> 'workspacePersistence' = 'snapshot_directory'
294
+ )
295
+ )
296
+ ),
297
+ CONSTRAINT sandbox_checkpoint_artifacts_state_check CHECK (
298
+ state IN (
299
+ 'candidate', 'current', 'previous', 'delete_pending',
300
+ 'deleting', 'delete_failed', 'deleted'
301
+ )
302
+ ),
303
+ CONSTRAINT sandbox_checkpoint_artifacts_delete_claim_check CHECK (
304
+ (state = 'deleting' AND delete_claim_id IS NOT NULL AND delete_claimed_at IS NOT NULL)
305
+ OR (state <> 'deleting' AND delete_claim_id IS NULL AND delete_claimed_at IS NULL)
306
+ )
307
+ );
308
+
309
+ CREATE UNIQUE INDEX sandbox_checkpoint_artifacts_provider_object_uq
310
+ ON sandbox_checkpoint_artifacts (provider_backend, provider_binding_key, object_id);
311
+ CREATE INDEX sandbox_checkpoint_artifacts_gc_idx
312
+ ON sandbox_checkpoint_artifacts (delete_after, created_at, id)
313
+ WHERE state IN (
314
+ 'candidate', 'current', 'previous',
315
+ 'delete_pending', 'delete_failed', 'deleting'
316
+ );
317
+ CREATE INDEX sandbox_checkpoint_artifacts_source_idx
318
+ ON sandbox_checkpoint_artifacts (workspace_id, sandbox_group_id, source_lease_id);
319
+
320
+ ALTER TABLE sandbox_leases
321
+ ADD COLUMN provider_created_at timestamptz,
322
+ ADD COLUMN provider_deadline_at timestamptz,
323
+ ADD COLUMN rotation_requested_at timestamptz,
324
+ ADD COLUMN rotation_reason text,
325
+ ADD COLUMN current_checkpoint_artifact_id uuid,
326
+ ADD COLUMN previous_checkpoint_artifact_id uuid;
327
+
328
+ ALTER TABLE sandbox_leases
329
+ ADD CONSTRAINT sandbox_leases_provider_deadline_check CHECK (
330
+ (provider_created_at IS NULL AND provider_deadline_at IS NULL)
331
+ OR (
332
+ provider_created_at IS NOT NULL
333
+ AND provider_deadline_at IS NOT NULL
334
+ AND provider_deadline_at > provider_created_at
335
+ )
336
+ ),
337
+ ADD CONSTRAINT sandbox_leases_rotation_check CHECK (
338
+ (rotation_requested_at IS NULL AND rotation_reason IS NULL)
339
+ OR (
340
+ rotation_requested_at IS NOT NULL
341
+ AND rotation_reason IN ('provider_deadline', 'operator')
342
+ )
343
+ ),
344
+ ADD CONSTRAINT sandbox_leases_checkpoint_distinct_check CHECK (
345
+ current_checkpoint_artifact_id IS NULL
346
+ OR previous_checkpoint_artifact_id IS NULL
347
+ OR current_checkpoint_artifact_id <> previous_checkpoint_artifact_id
348
+ ),
349
+ ADD CONSTRAINT sandbox_leases_current_checkpoint_fk
350
+ FOREIGN KEY (current_checkpoint_artifact_id)
351
+ REFERENCES sandbox_checkpoint_artifacts (id)
352
+ DEFERRABLE INITIALLY IMMEDIATE,
353
+ ADD CONSTRAINT sandbox_leases_previous_checkpoint_fk
354
+ FOREIGN KEY (previous_checkpoint_artifact_id)
355
+ REFERENCES sandbox_checkpoint_artifacts (id)
356
+ DEFERRABLE INITIALLY IMMEDIATE;
357
+
358
+ CREATE INDEX sandbox_leases_provider_deadline_idx
359
+ ON sandbox_leases (provider_deadline_at, id)
360
+ WHERE backend = 'modal'
361
+ AND liveness IN ('warming', 'warm')
362
+ AND rotation_requested_at IS NULL;
363
+
364
+ -- Publication validation is deferred so one transaction can register a
365
+ -- candidate, rotate both lease references, and update artifact states in any
366
+ -- convenient statement order while still rejecting cross-scope references.
367
+ DO $checkpoint_ref_validator$
368
+ DECLARE data_schema text := current_schema();
369
+ BEGIN
370
+ EXECUTE format($create$
371
+ CREATE OR REPLACE FUNCTION opengeni_private.validate_sandbox_checkpoint_refs()
372
+ RETURNS trigger
373
+ LANGUAGE plpgsql
374
+ SECURITY DEFINER
375
+ SET search_path = pg_catalog
376
+ AS $function$
377
+ DECLARE checkpoint %1$I.sandbox_checkpoint_artifacts%%ROWTYPE;
378
+ BEGIN
379
+ IF NEW.current_checkpoint_artifact_id IS NOT NULL THEN
380
+ SELECT * INTO checkpoint
381
+ FROM %1$I.sandbox_checkpoint_artifacts
382
+ WHERE id = NEW.current_checkpoint_artifact_id;
383
+ IF NOT FOUND
384
+ OR checkpoint.account_id <> NEW.account_id
385
+ OR checkpoint.workspace_id <> NEW.workspace_id
386
+ OR checkpoint.sandbox_group_id <> NEW.sandbox_group_id
387
+ OR checkpoint.source_lease_id <> NEW.id
388
+ OR checkpoint.provider_backend <> NEW.backend
389
+ OR (
390
+ checkpoint.provenance = 'native_capture'
391
+ AND checkpoint.source_workspace_generation
392
+ IS DISTINCT FROM NEW.archive_generation
393
+ )
394
+ OR (
395
+ checkpoint.provenance = 'legacy_provider_adopted'
396
+ AND NEW.archive_generation IS DISTINCT FROM NEW.workspace_generation
397
+ )
398
+ OR checkpoint.archive_base64
399
+ IS DISTINCT FROM NEW.resume_state #>> '{sessionState,workspaceArchive}'
400
+ OR checkpoint.descriptor_revision
401
+ IS DISTINCT FROM NEW.resume_state #>> '{sessionState,workspaceArchiveMeta,revision}'
402
+ OR checkpoint.state <> 'current'
403
+ THEN
404
+ RAISE EXCEPTION 'current checkpoint artifact does not match its exact lease scope'
405
+ USING ERRCODE = '23514';
406
+ END IF;
407
+ END IF;
408
+
409
+ IF NEW.previous_checkpoint_artifact_id IS NOT NULL THEN
410
+ SELECT * INTO checkpoint
411
+ FROM %1$I.sandbox_checkpoint_artifacts
412
+ WHERE id = NEW.previous_checkpoint_artifact_id;
413
+ IF NOT FOUND
414
+ OR checkpoint.account_id <> NEW.account_id
415
+ OR checkpoint.workspace_id <> NEW.workspace_id
416
+ OR checkpoint.sandbox_group_id <> NEW.sandbox_group_id
417
+ OR checkpoint.source_lease_id <> NEW.id
418
+ OR checkpoint.provider_backend <> NEW.backend
419
+ OR checkpoint.archive_base64
420
+ IS DISTINCT FROM NEW.resume_state #>> '{sessionState,workspaceArchivePrev}'
421
+ OR checkpoint.descriptor_revision
422
+ IS DISTINCT FROM NEW.resume_state #>> '{sessionState,workspaceArchivePrevMeta,revision}'
423
+ OR checkpoint.state <> 'previous'
424
+ THEN
425
+ RAISE EXCEPTION 'previous checkpoint artifact does not match its exact lease scope'
426
+ USING ERRCODE = '23514';
427
+ END IF;
428
+ END IF;
429
+ RETURN NEW;
430
+ END;
431
+ $function$;
432
+ $create$, data_schema);
433
+ END
434
+ $checkpoint_ref_validator$;
435
+
436
+ DO $checkpoint_artifact_ref_validator$
437
+ DECLARE data_schema text := current_schema();
438
+ BEGIN
439
+ EXECUTE format($create$
440
+ CREATE OR REPLACE FUNCTION opengeni_private.validate_sandbox_checkpoint_artifact_refs()
441
+ RETURNS trigger
442
+ LANGUAGE plpgsql
443
+ SECURITY DEFINER
444
+ SET search_path = pg_catalog
445
+ AS $function$
446
+ DECLARE exact_current boolean;
447
+ DECLARE exact_previous boolean;
448
+ DECLARE any_reference boolean;
449
+ DECLARE final_state text;
450
+ BEGIN
451
+ SELECT artifact.state INTO final_state
452
+ FROM %1$I.sandbox_checkpoint_artifacts artifact
453
+ WHERE artifact.id = NEW.id;
454
+ IF NOT FOUND THEN
455
+ RAISE EXCEPTION 'checkpoint artifact vanished before reference validation'
456
+ USING ERRCODE = '23514';
457
+ END IF;
458
+
459
+ SELECT
460
+ pg_catalog.bool_or(
461
+ lease.current_checkpoint_artifact_id = NEW.id
462
+ AND lease.id = NEW.source_lease_id
463
+ AND lease.account_id = NEW.account_id
464
+ AND lease.workspace_id = NEW.workspace_id
465
+ AND lease.sandbox_group_id = NEW.sandbox_group_id
466
+ AND lease.backend = NEW.provider_backend
467
+ ),
468
+ pg_catalog.bool_or(
469
+ lease.previous_checkpoint_artifact_id = NEW.id
470
+ AND lease.id = NEW.source_lease_id
471
+ AND lease.account_id = NEW.account_id
472
+ AND lease.workspace_id = NEW.workspace_id
473
+ AND lease.sandbox_group_id = NEW.sandbox_group_id
474
+ AND lease.backend = NEW.provider_backend
475
+ ),
476
+ pg_catalog.bool_or(
477
+ lease.current_checkpoint_artifact_id = NEW.id
478
+ OR lease.previous_checkpoint_artifact_id = NEW.id
479
+ )
480
+ INTO exact_current, exact_previous, any_reference
481
+ FROM %1$I.sandbox_leases lease
482
+ WHERE lease.current_checkpoint_artifact_id = NEW.id
483
+ OR lease.previous_checkpoint_artifact_id = NEW.id;
484
+
485
+ exact_current := coalesce(exact_current, false);
486
+ exact_previous := coalesce(exact_previous, false);
487
+ any_reference := coalesce(any_reference, false);
488
+
489
+ IF (final_state = 'current' AND (NOT exact_current OR exact_previous))
490
+ OR (final_state = 'previous' AND (NOT exact_previous OR exact_current))
491
+ OR (final_state NOT IN ('current', 'previous') AND any_reference)
492
+ THEN
493
+ RAISE EXCEPTION 'checkpoint artifact state does not match its exact lease reference'
494
+ USING ERRCODE = '23514';
495
+ END IF;
496
+ RETURN NEW;
497
+ END;
498
+ $function$;
499
+ $create$, data_schema);
500
+ END
501
+ $checkpoint_artifact_ref_validator$;
502
+
503
+ CREATE CONSTRAINT TRIGGER sandbox_checkpoint_refs_guard
504
+ AFTER INSERT OR UPDATE OF
505
+ account_id, workspace_id, sandbox_group_id, backend,
506
+ workspace_generation, archive_generation, resume_state,
507
+ current_checkpoint_artifact_id, previous_checkpoint_artifact_id
508
+ ON sandbox_leases
509
+ DEFERRABLE INITIALLY DEFERRED
510
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.validate_sandbox_checkpoint_refs();
511
+
512
+ CREATE CONSTRAINT TRIGGER sandbox_checkpoint_artifact_refs_guard
513
+ AFTER INSERT OR UPDATE OF
514
+ account_id, workspace_id, sandbox_group_id, source_lease_id,
515
+ provider_backend, state
516
+ ON sandbox_checkpoint_artifacts
517
+ DEFERRABLE INITIALLY DEFERRED
518
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.validate_sandbox_checkpoint_artifact_refs();
519
+
520
+ CREATE OR REPLACE FUNCTION opengeni_private.enforce_sandbox_checkpoint_artifact_immutability()
521
+ RETURNS trigger
522
+ LANGUAGE plpgsql
523
+ SET search_path = pg_catalog
524
+ AS $function$
525
+ BEGIN
526
+ IF ROW(
527
+ NEW.id, NEW.account_id, NEW.workspace_id, NEW.sandbox_group_id,
528
+ NEW.source_lease_id, NEW.source_lease_epoch, NEW.source_instance_id,
529
+ NEW.source_workspace_generation, NEW.provenance, NEW.provider_backend,
530
+ NEW.provider_binding_key, NEW.provider_binding, NEW.object_kind,
531
+ NEW.object_id, NEW.archive_base64, NEW.archive_sha256, NEW.archive_bytes,
532
+ NEW.descriptor, NEW.descriptor_revision, NEW.created_at
533
+ ) IS DISTINCT FROM ROW(
534
+ OLD.id, OLD.account_id, OLD.workspace_id, OLD.sandbox_group_id,
535
+ OLD.source_lease_id, OLD.source_lease_epoch, OLD.source_instance_id,
536
+ OLD.source_workspace_generation, OLD.provenance, OLD.provider_backend,
537
+ OLD.provider_binding_key, OLD.provider_binding, OLD.object_kind,
538
+ OLD.object_id, OLD.archive_base64, OLD.archive_sha256, OLD.archive_bytes,
539
+ OLD.descriptor, OLD.descriptor_revision, OLD.created_at
540
+ )
541
+ THEN
542
+ RAISE EXCEPTION 'checkpoint artifact identity and receipt fields are immutable'
543
+ USING ERRCODE = '55000';
544
+ END IF;
545
+ RETURN NEW;
546
+ END
547
+ $function$;
548
+
549
+ CREATE TRIGGER sandbox_checkpoint_artifact_immutability_guard
550
+ BEFORE UPDATE ON sandbox_checkpoint_artifacts
551
+ FOR EACH ROW
552
+ EXECUTE FUNCTION opengeni_private.enforce_sandbox_checkpoint_artifact_immutability();
553
+
554
+ CREATE TRIGGER sandbox_recovery_protocol_v2_checkpoint_guard
555
+ BEFORE INSERT OR UPDATE OR DELETE ON sandbox_checkpoint_artifacts
556
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.enforce_sandbox_recovery_protocol_v2();
557
+
558
+ ALTER TABLE sandbox_checkpoint_artifacts ENABLE ROW LEVEL SECURITY;
559
+ ALTER TABLE sandbox_checkpoint_artifacts FORCE ROW LEVEL SECURITY;
560
+ CREATE POLICY workspace_isolation ON sandbox_checkpoint_artifacts
561
+ USING (opengeni_private.workspace_rls_visible(account_id, workspace_id))
562
+ WITH CHECK (opengeni_private.workspace_rls_visible(account_id, workspace_id));
563
+
564
+ -- Cross-workspace claim. Candidates that were captured but never published are
565
+ -- collectible after a grace period. Referenced artifacts are never returned,
566
+ -- even if a corrupt/stale state label says otherwise.
567
+ DO $checkpoint_gc_functions$
568
+ DECLARE data_schema text := current_schema();
569
+ BEGIN
570
+ EXECUTE format($create$
571
+ CREATE OR REPLACE FUNCTION opengeni_private.claim_sandbox_checkpoint_artifacts(
572
+ p_claim_id uuid,
573
+ p_limit integer,
574
+ p_claim_ttl_ms bigint
575
+ )
576
+ RETURNS TABLE (
577
+ id uuid,
578
+ provider_backend text,
579
+ provider_binding_key text,
580
+ provider_binding jsonb,
581
+ object_kind text,
582
+ object_id text,
583
+ delete_attempts integer
584
+ )
585
+ LANGUAGE plpgsql
586
+ SECURITY DEFINER
587
+ SET search_path = pg_catalog
588
+ AS $function$
589
+ BEGIN
590
+ IF p_claim_id IS NULL THEN
591
+ RAISE EXCEPTION 'checkpoint artifact claim id is required'
592
+ USING ERRCODE = '22023';
593
+ END IF;
594
+ IF p_limit IS NULL OR p_limit < 1 OR p_limit > 500 THEN
595
+ RAISE EXCEPTION 'checkpoint artifact claim limit must be between 1 and 500'
596
+ USING ERRCODE = '22023';
597
+ END IF;
598
+ IF p_claim_ttl_ms IS NULL
599
+ OR p_claim_ttl_ms < 1000
600
+ OR p_claim_ttl_ms > 3600000
601
+ THEN
602
+ RAISE EXCEPTION 'checkpoint artifact claim TTL must be between 1s and 1h'
603
+ USING ERRCODE = '22023';
604
+ END IF;
605
+
606
+ PERFORM pg_catalog.set_config('opengeni.sandbox_recovery_protocol_v2', '1', true);
607
+
608
+ WITH stale_claims AS MATERIALIZED (
609
+ SELECT artifact.id
610
+ FROM %1$I.sandbox_checkpoint_artifacts artifact
611
+ WHERE artifact.state = 'deleting'
612
+ AND artifact.delete_claimed_at
613
+ < pg_catalog.now()
614
+ - pg_catalog.make_interval(secs => p_claim_ttl_ms / 1000.0)
615
+ ORDER BY artifact.delete_claimed_at, artifact.id
616
+ FOR UPDATE SKIP LOCKED
617
+ LIMIT p_limit
618
+ )
619
+ UPDATE %1$I.sandbox_checkpoint_artifacts artifact SET
620
+ state = 'delete_failed',
621
+ delete_after = pg_catalog.now(),
622
+ delete_claim_id = null,
623
+ delete_claimed_at = null,
624
+ last_delete_error = coalesce(
625
+ last_delete_error, 'stale delete claim recovered'
626
+ ),
627
+ updated_at = pg_catalog.now()
628
+ FROM stale_claims
629
+ WHERE artifact.id = stale_claims.id;
630
+
631
+ RETURN QUERY
632
+ WITH candidates AS (
633
+ SELECT artifact.id
634
+ FROM %1$I.sandbox_checkpoint_artifacts artifact
635
+ WHERE (
636
+ (
637
+ artifact.state = 'candidate'
638
+ AND artifact.created_at < pg_catalog.now() - interval '15 minutes'
639
+ )
640
+ OR (
641
+ artifact.state IN ('current', 'previous')
642
+ AND artifact.created_at < pg_catalog.now() - interval '15 minutes'
643
+ )
644
+ OR (
645
+ artifact.state IN ('delete_pending', 'delete_failed')
646
+ AND coalesce(artifact.delete_after, artifact.created_at)
647
+ <= pg_catalog.now()
648
+ )
649
+ )
650
+ AND NOT EXISTS (
651
+ SELECT 1 FROM %1$I.sandbox_leases lease
652
+ WHERE lease.current_checkpoint_artifact_id = artifact.id
653
+ OR lease.previous_checkpoint_artifact_id = artifact.id
654
+ )
655
+ ORDER BY coalesce(artifact.delete_after, artifact.created_at), artifact.id
656
+ FOR UPDATE SKIP LOCKED
657
+ LIMIT p_limit
658
+ )
659
+ UPDATE %1$I.sandbox_checkpoint_artifacts artifact SET
660
+ state = 'deleting',
661
+ delete_attempts = artifact.delete_attempts + 1,
662
+ delete_claim_id = p_claim_id,
663
+ delete_claimed_at = pg_catalog.now(),
664
+ updated_at = pg_catalog.now()
665
+ FROM candidates
666
+ WHERE artifact.id = candidates.id
667
+ RETURNING artifact.id, artifact.provider_backend, artifact.provider_binding_key,
668
+ artifact.provider_binding, artifact.object_kind, artifact.object_id,
669
+ artifact.delete_attempts;
670
+ END;
671
+ $function$;
672
+ $create$, data_schema);
673
+
674
+ EXECUTE format($create$
675
+ CREATE OR REPLACE FUNCTION opengeni_private.settle_sandbox_checkpoint_artifact(
676
+ p_id uuid,
677
+ p_claim_id uuid,
678
+ p_deleted boolean,
679
+ p_error text,
680
+ p_retry_after_ms bigint
681
+ )
682
+ RETURNS boolean
683
+ LANGUAGE plpgsql
684
+ SECURITY DEFINER
685
+ SET search_path = pg_catalog
686
+ AS $function$
687
+ DECLARE changed integer;
688
+ BEGIN
689
+ IF p_id IS NULL OR p_claim_id IS NULL THEN
690
+ RAISE EXCEPTION 'checkpoint artifact and claim ids are required'
691
+ USING ERRCODE = '22023';
692
+ END IF;
693
+ IF p_deleted IS NULL THEN
694
+ RAISE EXCEPTION 'checkpoint artifact deletion outcome is required'
695
+ USING ERRCODE = '22023';
696
+ END IF;
697
+ IF p_retry_after_ms IS NULL
698
+ OR p_retry_after_ms < 0
699
+ OR p_retry_after_ms > 86400000
700
+ THEN
701
+ RAISE EXCEPTION 'checkpoint artifact retry delay must be between 0 and 24h'
702
+ USING ERRCODE = '22023';
703
+ END IF;
704
+
705
+ PERFORM pg_catalog.set_config('opengeni.sandbox_recovery_protocol_v2', '1', true);
706
+ UPDATE %1$I.sandbox_checkpoint_artifacts SET
707
+ state = CASE WHEN p_deleted THEN 'deleted' ELSE 'delete_failed' END,
708
+ deleted_at = CASE WHEN p_deleted THEN pg_catalog.now() ELSE null END,
709
+ delete_after = CASE
710
+ WHEN p_deleted THEN null
711
+ ELSE pg_catalog.now()
712
+ + pg_catalog.make_interval(secs => greatest(1000, p_retry_after_ms) / 1000.0)
713
+ END,
714
+ delete_claim_id = null,
715
+ delete_claimed_at = null,
716
+ last_delete_error = CASE
717
+ WHEN p_deleted THEN null
718
+ ELSE pg_catalog.left(coalesce(p_error, 'unknown'), 4000)
719
+ END,
720
+ updated_at = pg_catalog.now()
721
+ WHERE id = p_id
722
+ AND state = 'deleting'
723
+ AND delete_claim_id = p_claim_id;
724
+ GET DIAGNOSTICS changed = ROW_COUNT;
725
+ RETURN changed = 1;
726
+ END;
727
+ $function$;
728
+ $create$, data_schema);
729
+
730
+ EXECUTE format($create$
731
+ CREATE OR REPLACE FUNCTION opengeni_private.prune_deleted_sandbox_checkpoint_artifacts(
732
+ p_retention_ms bigint,
733
+ p_limit integer
734
+ )
735
+ RETURNS integer
736
+ LANGUAGE plpgsql
737
+ SECURITY DEFINER
738
+ SET search_path = pg_catalog
739
+ AS $function$
740
+ DECLARE pruned integer;
741
+ BEGIN
742
+ IF p_retention_ms IS NULL
743
+ OR p_retention_ms < 86400000
744
+ OR p_retention_ms > 31536000000
745
+ THEN
746
+ RAISE EXCEPTION 'checkpoint tombstone retention must be between 1 and 365 days'
747
+ USING ERRCODE = '22023';
748
+ END IF;
749
+ IF p_limit IS NULL OR p_limit < 1 OR p_limit > 1000 THEN
750
+ RAISE EXCEPTION 'checkpoint tombstone prune limit must be between 1 and 1000'
751
+ USING ERRCODE = '22023';
752
+ END IF;
753
+
754
+ PERFORM pg_catalog.set_config('opengeni.sandbox_recovery_protocol_v2', '1', true);
755
+ WITH candidates AS MATERIALIZED (
756
+ SELECT artifact.id
757
+ FROM %1$I.sandbox_checkpoint_artifacts artifact
758
+ WHERE artifact.state = 'deleted'
759
+ AND artifact.deleted_at
760
+ < pg_catalog.now()
761
+ - pg_catalog.make_interval(secs => p_retention_ms / 1000.0)
762
+ AND NOT EXISTS (
763
+ SELECT 1 FROM %1$I.sandbox_leases lease
764
+ WHERE lease.current_checkpoint_artifact_id = artifact.id
765
+ OR lease.previous_checkpoint_artifact_id = artifact.id
766
+ )
767
+ ORDER BY artifact.deleted_at, artifact.id
768
+ FOR UPDATE SKIP LOCKED
769
+ LIMIT p_limit
770
+ ),
771
+ removed AS (
772
+ DELETE FROM %1$I.sandbox_checkpoint_artifacts artifact
773
+ USING candidates
774
+ WHERE artifact.id = candidates.id
775
+ RETURNING artifact.id
776
+ )
777
+ SELECT pg_catalog.count(*)::integer INTO pruned FROM removed;
778
+ RETURN pruned;
779
+ END;
780
+ $function$;
781
+ $create$, data_schema);
782
+ END
783
+ $checkpoint_gc_functions$;
784
+
785
+ DO $checkpoint_legacy_and_rotation_functions$
786
+ DECLARE data_schema text := current_schema();
787
+ BEGIN
788
+ EXECUTE format($create$
789
+ CREATE OR REPLACE FUNCTION opengeni_private.list_legacy_modal_checkpoint_slots(
790
+ p_limit integer
791
+ )
792
+ RETURNS TABLE (
793
+ account_id uuid,
794
+ workspace_id uuid,
795
+ sandbox_group_id uuid,
796
+ lease_id uuid,
797
+ lease_epoch integer,
798
+ instance_id text,
799
+ workspace_generation integer,
800
+ slot text,
801
+ archive_base64 text,
802
+ descriptor jsonb
803
+ )
804
+ LANGUAGE plpgsql
805
+ SECURITY DEFINER
806
+ SET search_path = pg_catalog
807
+ AS $function$
808
+ BEGIN
809
+ IF p_limit IS NULL OR p_limit < 1 OR p_limit > 500 THEN
810
+ RAISE EXCEPTION 'legacy checkpoint slot limit must be between 1 and 500'
811
+ USING ERRCODE = '22023';
812
+ END IF;
813
+
814
+ RETURN QUERY
815
+ SELECT lease.account_id, lease.workspace_id, lease.sandbox_group_id, lease.id,
816
+ lease.lease_epoch, lease.instance_id,
817
+ lease.workspace_generation, candidate.slot, candidate.archive_base64,
818
+ candidate.descriptor
819
+ FROM %1$I.sandbox_leases lease
820
+ CROSS JOIN LATERAL (
821
+ VALUES
822
+ (
823
+ 'current'::text,
824
+ lease.resume_state #>> '{sessionState,workspaceArchive}',
825
+ lease.resume_state #> '{sessionState,workspaceArchiveMeta}',
826
+ lease.current_checkpoint_artifact_id
827
+ ),
828
+ (
829
+ 'previous'::text,
830
+ lease.resume_state #>> '{sessionState,workspaceArchivePrev}',
831
+ lease.resume_state #> '{sessionState,workspaceArchivePrevMeta}',
832
+ lease.previous_checkpoint_artifact_id
833
+ )
834
+ ) candidate(slot, archive_base64, descriptor, artifact_id)
835
+ WHERE candidate.artifact_id IS NULL
836
+ AND lease.backend = 'modal'
837
+ AND lease.liveness IN ('warming', 'warm', 'draining')
838
+ AND lease.instance_id IS NOT NULL
839
+ AND coalesce(candidate.archive_base64, '') <> ''
840
+ AND candidate.descriptor ->> 'version' = '2'
841
+ AND candidate.descriptor ->> 'provider'
842
+ IN ('modal_snapshot_filesystem', 'modal_snapshot_directory')
843
+ ORDER BY lease.updated_at, lease.id, candidate.slot
844
+ LIMIT p_limit;
845
+ END;
846
+ $function$;
847
+ $create$, data_schema);
848
+
849
+ -- Request controlled rotation before Modal's hard creation-time timeout.
850
+ -- Existing passive viewer holders are evicted because they cannot mutate
851
+ -- workspace state and may reconnect to the successor. Direct API operations
852
+ -- can mutate files/git/terminal state, so they remain blockers until their
853
+ -- normal release or the existing direct-holder TTL reaper proves them stale.
854
+ -- Turn/process holders likewise remain until their explicit settlement path.
855
+ EXECUTE format($create$
856
+ CREATE OR REPLACE FUNCTION opengeni_private.request_due_sandbox_rotations(
857
+ p_lead_ms bigint,
858
+ p_limit integer
859
+ )
860
+ RETURNS integer
861
+ LANGUAGE plpgsql
862
+ SECURITY DEFINER
863
+ SET search_path = pg_catalog
864
+ AS $function$
865
+ DECLARE
866
+ requested integer;
867
+ requested_ids uuid[];
868
+ BEGIN
869
+ IF p_lead_ms IS NULL OR p_lead_ms < 0 OR p_lead_ms > 86400000 THEN
870
+ RAISE EXCEPTION 'sandbox rotation lead must be between 0 and 24h'
871
+ USING ERRCODE = '22023';
872
+ END IF;
873
+ IF p_limit IS NULL OR p_limit < 1 OR p_limit > 500 THEN
874
+ RAISE EXCEPTION 'sandbox rotation batch limit must be between 1 and 500'
875
+ USING ERRCODE = '22023';
876
+ END IF;
877
+
878
+ PERFORM pg_catalog.set_config('opengeni.sandbox_recovery_protocol_v2', '1', true);
879
+
880
+ WITH candidates AS MATERIALIZED (
881
+ SELECT lease.id
882
+ FROM %1$I.sandbox_leases lease
883
+ WHERE lease.backend = 'modal'
884
+ AND lease.liveness IN ('warming', 'warm')
885
+ AND lease.provider_deadline_at IS NOT NULL
886
+ AND lease.provider_deadline_at
887
+ <= pg_catalog.now()
888
+ + pg_catalog.make_interval(secs => p_lead_ms / 1000.0)
889
+ AND lease.rotation_requested_at IS NULL
890
+ ORDER BY lease.provider_deadline_at, lease.id
891
+ FOR UPDATE SKIP LOCKED
892
+ LIMIT p_limit
893
+ ),
894
+ due AS (
895
+ UPDATE %1$I.sandbox_leases lease SET
896
+ rotation_requested_at = pg_catalog.now(),
897
+ rotation_reason = 'provider_deadline',
898
+ updated_at = pg_catalog.now()
899
+ FROM candidates
900
+ WHERE lease.id = candidates.id
901
+ RETURNING lease.id
902
+ )
903
+ SELECT
904
+ coalesce(pg_catalog.array_agg(due.id), ARRAY[]::uuid[]),
905
+ pg_catalog.count(*)::integer
906
+ INTO requested_ids, requested
907
+ FROM due;
908
+
909
+ DELETE FROM %1$I.sandbox_lease_holders holder
910
+ WHERE holder.lease_id = ANY(requested_ids)
911
+ AND holder.kind = 'viewer';
912
+
913
+ UPDATE %1$I.sandbox_leases lease SET
914
+ refcount = counts.total,
915
+ turn_holders = counts.turns,
916
+ viewer_holders = counts.viewers,
917
+ liveness = CASE
918
+ WHEN lease.liveness = 'warm' AND counts.total = 0 THEN 'draining'
919
+ ELSE lease.liveness
920
+ END,
921
+ expires_at = CASE
922
+ WHEN lease.liveness = 'warm' AND counts.total = 0
923
+ THEN pg_catalog.now() - interval '1 millisecond'
924
+ ELSE lease.expires_at
925
+ END,
926
+ updated_at = pg_catalog.now()
927
+ FROM (
928
+ SELECT candidate.id,
929
+ (SELECT pg_catalog.count(*) FROM %1$I.sandbox_lease_holders holder
930
+ WHERE holder.lease_id = candidate.id)::int AS total,
931
+ (SELECT pg_catalog.count(*) FROM %1$I.sandbox_lease_holders holder
932
+ WHERE holder.lease_id = candidate.id AND holder.kind = 'turn')::int AS turns,
933
+ (SELECT pg_catalog.count(*) FROM %1$I.sandbox_lease_holders holder
934
+ WHERE holder.lease_id = candidate.id AND holder.kind = 'viewer')::int AS viewers
935
+ FROM unnest(requested_ids) requested_id(id)
936
+ JOIN %1$I.sandbox_leases candidate ON candidate.id = requested_id.id
937
+ ) counts
938
+ WHERE lease.id = counts.id;
939
+
940
+ RETURN requested;
941
+ END;
942
+ $function$;
943
+ $create$, data_schema);
944
+
945
+ EXECUTE format($create$
946
+ CREATE OR REPLACE FUNCTION opengeni_private.sandbox_checkpoint_artifact_inventory()
947
+ RETURNS TABLE (state text, count bigint)
948
+ LANGUAGE sql
949
+ SECURITY DEFINER
950
+ SET search_path = pg_catalog
951
+ AS $function$
952
+ SELECT artifact.state, pg_catalog.count(*)::bigint
953
+ FROM %1$I.sandbox_checkpoint_artifacts artifact
954
+ GROUP BY artifact.state
955
+ ORDER BY artifact.state;
956
+ $function$;
957
+ $create$, data_schema);
958
+
959
+ EXECUTE format($create$
960
+ CREATE OR REPLACE FUNCTION opengeni_private.sandbox_rotation_backlog()
961
+ RETURNS TABLE (
962
+ requested bigint,
963
+ overdue bigint,
964
+ turn_blocked bigint,
965
+ direct_blocked bigint,
966
+ process_blocked bigint
967
+ )
968
+ LANGUAGE sql
969
+ SECURITY DEFINER
970
+ SET search_path = pg_catalog
971
+ AS $function$
972
+ SELECT
973
+ pg_catalog.count(*) FILTER (
974
+ WHERE lease.rotation_requested_at IS NOT NULL
975
+ )::bigint,
976
+ pg_catalog.count(*) FILTER (
977
+ WHERE lease.rotation_requested_at IS NOT NULL
978
+ AND lease.provider_deadline_at <= pg_catalog.now()
979
+ )::bigint,
980
+ pg_catalog.count(*) FILTER (
981
+ WHERE lease.rotation_requested_at IS NOT NULL
982
+ AND EXISTS (
983
+ SELECT 1 FROM %1$I.sandbox_lease_holders holder
984
+ WHERE holder.lease_id = lease.id AND holder.kind = 'turn'
985
+ )
986
+ )::bigint,
987
+ pg_catalog.count(*) FILTER (
988
+ WHERE lease.rotation_requested_at IS NOT NULL
989
+ AND EXISTS (
990
+ SELECT 1 FROM %1$I.sandbox_lease_holders holder
991
+ WHERE holder.lease_id = lease.id AND holder.kind = 'direct'
992
+ )
993
+ )::bigint,
994
+ pg_catalog.count(*) FILTER (
995
+ WHERE lease.rotation_requested_at IS NOT NULL
996
+ AND EXISTS (
997
+ SELECT 1 FROM %1$I.sandbox_lease_holders holder
998
+ WHERE holder.lease_id = lease.id AND holder.kind = 'process'
999
+ )
1000
+ )::bigint
1001
+ FROM %1$I.sandbox_leases lease
1002
+ WHERE lease.backend = 'modal'
1003
+ AND lease.liveness IN ('warming', 'warm', 'draining');
1004
+ $function$;
1005
+ $create$, data_schema);
1006
+ END
1007
+ $checkpoint_legacy_and_rotation_functions$;
1008
+
1009
+ -- Existing live Modal leases predate durable provider creation clocks. Their
1010
+ -- true remaining lifetime is unknowable, so fail safe: make them immediately
1011
+ -- eligible for controlled rotation after the new worker starts.
1012
+ UPDATE sandbox_leases
1013
+ SET provider_created_at = now() - interval '1 millisecond',
1014
+ provider_deadline_at = now(),
1015
+ updated_at = now()
1016
+ WHERE backend = 'modal'
1017
+ AND liveness IN ('warming', 'warm', 'draining')
1018
+ AND instance_id IS NOT NULL
1019
+ AND provider_deadline_at IS NULL;
1020
+
1021
+ -- PostgreSQL grants new functions to PUBLIC by default. Every function below
1022
+ -- either bypasses workspace RLS or is an internal trigger implementation, so
1023
+ -- leave execution to the migration owner and explicitly granted application
1024
+ -- role only.
1025
+ REVOKE ALL ON FUNCTION opengeni_private.validate_sandbox_checkpoint_refs()
1026
+ FROM PUBLIC;
1027
+ REVOKE ALL ON FUNCTION opengeni_private.validate_sandbox_checkpoint_artifact_refs()
1028
+ FROM PUBLIC;
1029
+ REVOKE ALL ON FUNCTION opengeni_private.enforce_sandbox_checkpoint_artifact_immutability()
1030
+ FROM PUBLIC;
1031
+ REVOKE ALL ON FUNCTION opengeni_private.claim_sandbox_checkpoint_artifacts(uuid, integer, bigint)
1032
+ FROM PUBLIC;
1033
+ REVOKE ALL ON FUNCTION opengeni_private.settle_sandbox_checkpoint_artifact(uuid, uuid, boolean, text, bigint)
1034
+ FROM PUBLIC;
1035
+ REVOKE ALL ON FUNCTION opengeni_private.prune_deleted_sandbox_checkpoint_artifacts(bigint, integer)
1036
+ FROM PUBLIC;
1037
+ REVOKE ALL ON FUNCTION opengeni_private.list_legacy_modal_checkpoint_slots(integer)
1038
+ FROM PUBLIC;
1039
+ REVOKE ALL ON FUNCTION opengeni_private.request_due_sandbox_rotations(bigint, integer)
1040
+ FROM PUBLIC;
1041
+ REVOKE ALL ON FUNCTION opengeni_private.sandbox_checkpoint_artifact_inventory()
1042
+ FROM PUBLIC;
1043
+ REVOKE ALL ON FUNCTION opengeni_private.sandbox_rotation_backlog()
1044
+ FROM PUBLIC;
1045
+
1046
+ DO $grants$
1047
+ DECLARE data_schema text := current_schema();
1048
+ BEGIN
1049
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
1050
+ EXECUTE format(
1051
+ 'GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE %I.sandbox_checkpoint_artifacts TO opengeni_app',
1052
+ data_schema
1053
+ );
1054
+ EXECUTE format(
1055
+ 'GRANT SELECT, UPDATE ON TABLE %I.sandbox_leases TO opengeni_app',
1056
+ data_schema
1057
+ );
1058
+ GRANT EXECUTE ON FUNCTION opengeni_private.claim_sandbox_checkpoint_artifacts(uuid, integer, bigint)
1059
+ TO opengeni_app;
1060
+ GRANT EXECUTE ON FUNCTION opengeni_private.settle_sandbox_checkpoint_artifact(uuid, uuid, boolean, text, bigint)
1061
+ TO opengeni_app;
1062
+ GRANT EXECUTE ON FUNCTION opengeni_private.prune_deleted_sandbox_checkpoint_artifacts(bigint, integer)
1063
+ TO opengeni_app;
1064
+ GRANT EXECUTE ON FUNCTION opengeni_private.list_legacy_modal_checkpoint_slots(integer)
1065
+ TO opengeni_app;
1066
+ GRANT EXECUTE ON FUNCTION opengeni_private.request_due_sandbox_rotations(bigint, integer)
1067
+ TO opengeni_app;
1068
+ GRANT EXECUTE ON FUNCTION opengeni_private.sandbox_checkpoint_artifact_inventory()
1069
+ TO opengeni_app;
1070
+ GRANT EXECUTE ON FUNCTION opengeni_private.sandbox_rotation_backlog()
1071
+ TO opengeni_app;
1072
+ END IF;
1073
+ END
1074
+ $grants$;