@opengeni/db 0.2.0

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 (66) hide show
  1. package/dist/chunk-57MLICFR.js +121 -0
  2. package/dist/chunk-57MLICFR.js.map +1 -0
  3. package/dist/chunk-OGCE6O2X.js +52 -0
  4. package/dist/chunk-OGCE6O2X.js.map +1 -0
  5. package/dist/chunk-PSX56ZTL.js +1093 -0
  6. package/dist/chunk-PSX56ZTL.js.map +1 -0
  7. package/dist/chunk-PZ5AY32C.js +10 -0
  8. package/dist/chunk-PZ5AY32C.js.map +1 -0
  9. package/dist/index.d.ts +8 -0
  10. package/dist/index.js +5165 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/migrate.d.ts +40 -0
  13. package/dist/migrate.js +10 -0
  14. package/dist/migrate.js.map +1 -0
  15. package/dist/provision-roles.d.ts +2063 -0
  16. package/dist/provision-roles.js +8 -0
  17. package/dist/provision-roles.js.map +1 -0
  18. package/dist/schema-CaeZQAJQ.d.ts +9705 -0
  19. package/dist/schema.d.ts +3 -0
  20. package/dist/schema.js +110 -0
  21. package/dist/schema.js.map +1 -0
  22. package/drizzle/0000_initial.sql +179 -0
  23. package/drizzle/0001_workspace_auth_billing.sql +590 -0
  24. package/drizzle/0002_packs_and_social.sql +99 -0
  25. package/drizzle/0003_capability_catalog.sql +73 -0
  26. package/drizzle/0004_workspace_environments.sql +65 -0
  27. package/drizzle/0005_session_goals.sql +45 -0
  28. package/drizzle/0006_workspace_packs.sql +31 -0
  29. package/drizzle/0007_session_history_items.sql +66 -0
  30. package/drizzle/0008_session_first_party_mcp_permissions.sql +5 -0
  31. package/drizzle/0009_goal_sessions_first_party_goals_manage.sql +34 -0
  32. package/drizzle/0010_session_parent_linkage.sql +30 -0
  33. package/drizzle/0011_context_compaction.sql +33 -0
  34. package/drizzle/0012_compaction_summary_fractional_position.sql +19 -0
  35. package/drizzle/0013_session_compact_requested.sql +16 -0
  36. package/drizzle/0014_repair_orphaned_function_call_results.sql +125 -0
  37. package/drizzle/0015_workspace_agent_instructions.sql +17 -0
  38. package/drizzle/0016_session_create_idempotency.sql +27 -0
  39. package/drizzle/0017_sandbox_leases.sql +313 -0
  40. package/drizzle/0018_sandbox_os.sql +89 -0
  41. package/drizzle/0019_session_stream_acknowledgments.sql +94 -0
  42. package/drizzle/0020_session_recordings.sql +88 -0
  43. package/drizzle/0021_sandbox_pty_sessions.sql +70 -0
  44. package/drizzle/0022_sandbox_lease_terminal_url.sql +32 -0
  45. package/drizzle/0023_session_title.sql +19 -0
  46. package/drizzle/0024_codex_subscription_credentials.sql +51 -0
  47. package/drizzle/0024_sandboxes_enrollments_metrics.sql +262 -0
  48. package/drizzle/0025_device_enrollment_requests.sql +142 -0
  49. package/drizzle/0026_device_enrollment_user_code_resolver.sql +47 -0
  50. package/drizzle/0027_session_working_dir.sql +24 -0
  51. package/drizzle/0028_codex_multi_account.sql +85 -0
  52. package/drizzle/0029_session_history_item_producer.sql +31 -0
  53. package/drizzle/0030_agent_run_state_frozen_codex.sql +35 -0
  54. package/drizzle/0031_codex_usage_cache.sql +21 -0
  55. package/drizzle/0032_codex_account_cooldown.sql +18 -0
  56. package/drizzle/0033_codex_connector_cache.sql +20 -0
  57. package/drizzle/0034_sandbox_lease_image.sql +21 -0
  58. package/drizzle/meta/_journal.json +167 -0
  59. package/package.json +66 -0
  60. package/src/codex-token-resolver.ts +247 -0
  61. package/src/environment-crypto.ts +51 -0
  62. package/src/event-payload-sanitizer.ts +89 -0
  63. package/src/index.ts +7776 -0
  64. package/src/migrate.ts +95 -0
  65. package/src/provision-roles.ts +198 -0
  66. package/src/schema.ts +1110 -0
@@ -0,0 +1,17 @@
1
+ -- Per-workspace white-label agent persona override.
2
+ --
3
+ -- NULL means "use the deployment default template"
4
+ -- (OPENGENI_AGENT_INSTRUCTIONS_TEMPLATE / DEFAULT_AGENT_INSTRUCTIONS), so every
5
+ -- existing workspace keeps the historical, byte-identical preamble after this
6
+ -- migration without a backfill. The runtime always injects the non-bypassable
7
+ -- CORE (goal-loop ownership + workspace-environment block) regardless of this
8
+ -- value, so an override can restyle the persona but never drop that contract.
9
+ ALTER TABLE "workspaces"
10
+ ADD COLUMN IF NOT EXISTS "agent_instructions" text;
11
+
12
+ DO $$
13
+ BEGIN
14
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
15
+ GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO opengeni_app;
16
+ END IF;
17
+ END $$;
@@ -0,0 +1,27 @@
1
+ -- Workspace-scoped CREATE idempotency for sessions.
2
+ --
3
+ -- Closes the stuck-queued create-path bug: createSessionForRequest used to
4
+ -- unconditionally insert a brand-new session on every call, so a double-fire
5
+ -- (client double-submit / retry / double-dispatch) created duplicate sessions,
6
+ -- some stranded status=queued forever. clientEventId cannot dedup this — its
7
+ -- unique index is per-session, so two brand-new sessions never collide on it.
8
+ --
9
+ -- The new nullable key + PARTIAL unique index gives the create path a
10
+ -- workspace-scoped dedup target: concurrent creates carrying the same key
11
+ -- collapse to a single row (the loser sees a unique violation, which the
12
+ -- domain layer catches and turns into "return the existing session"). NULL
13
+ -- keys are exempt from the index, so back-compat (key-less) creates stay
14
+ -- independent and existing rows need no backfill.
15
+ ALTER TABLE "sessions"
16
+ ADD COLUMN IF NOT EXISTS "create_idempotency_key" text;
17
+
18
+ CREATE UNIQUE INDEX IF NOT EXISTS "sessions_workspace_create_idempotency_idx"
19
+ ON "sessions" ("workspace_id", "create_idempotency_key")
20
+ WHERE "create_idempotency_key" IS NOT NULL;
21
+
22
+ DO $$
23
+ BEGIN
24
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
25
+ GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO opengeni_app;
26
+ END IF;
27
+ END $$;
@@ -0,0 +1,313 @@
1
+ -- Sandbox singleton lease + refcounted holders + the cross-workspace reaper sweep.
2
+ --
3
+ -- The SOLE enforcer of one-box-per-group (P1.1; design-of-record
4
+ -- 08-implementation-plan.md P1.1 + modules/01-lease.md, re-keyed to
5
+ -- sandbox_group_id per addendum B.2). Two tables, authored GROUP-keyed from the
6
+ -- start so today's 1:1 world (sandbox_group_id == session id, set in 0018) is a
7
+ -- behavior-preserving no-op and the shared-sandbox surface (P1.4) needs no
8
+ -- re-key later.
9
+ --
10
+ -- The three Criticals land here together:
11
+ -- * CAS keys — every lease op keyed (workspace_id, sandbox_group_id).
12
+ -- * meter key — last_meter_at/last_meter_tick shape (warm_seconds accrues
13
+ -- idempotent on (sandbox_group_id, lease_epoch, last_meter_tick)
14
+ -- in P2.1; the column shape lands here).
15
+ -- * envelope split — resume_backend_id + resume_state fold the group's box
16
+ -- recovery envelope onto the lease (no per-session join).
17
+ --
18
+ -- lease_epoch is INTEGER (not bigint/int8): the lease-epoch spike proved
19
+ -- postgres-js returns int8 from a raw query as a JS STRING, so the strict
20
+ -- epoch-fence compare (row.lease_epoch !== expectedEpoch) was always-true and
21
+ -- fenced every turn; an integer/int4 column comes back as a JS number, which
22
+ -- restores the fence. Epochs never approach 2^31, so the narrower type loses
23
+ -- nothing.
24
+ --
25
+ -- sandbox_group_id is a BARE uuid, DELIBERATELY NOT an FK to sessions(id): the
26
+ -- value is a session id or an ancestor's id in the same workspace; an FK would
27
+ -- let a founder's deletion cascade-kill a box still in use by a spawned session.
28
+ -- The live lease row IS the materialization of "this group has a box" (there is
29
+ -- no sandbox_groups table).
30
+ --
31
+ -- DDL is INERT until P1.2 wires it (flag sandboxOwnershipEnabled); this
32
+ -- migration only creates the tables, indexes, RLS, grants, and the sweep fn.
33
+
34
+ -- ============== sandbox_leases (exactly one logical row per group) ===========
35
+ -- The UNIQUE (workspace_id, sandbox_group_id) index is the only hardware that
36
+ -- prevents a second box. CAS, epoch, and FOR UPDATE all layer on top of it.
37
+ CREATE TABLE IF NOT EXISTS "sandbox_leases" (
38
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
39
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE CASCADE,
40
+ "workspace_id" uuid NOT NULL REFERENCES "workspaces"("id") ON DELETE CASCADE,
41
+
42
+ -- The BOX's identity: a session id (singleton group) or an ancestor's id
43
+ -- (shared group). NOT an FK (see header). The lease is per-group, not
44
+ -- per-session; holders carry the attributing session_id.
45
+ "sandbox_group_id" uuid NOT NULL,
46
+
47
+ -- The 4-state machine. CHECK pins the domain at the DB edge so a buggy writer
48
+ -- cannot persist an off-grid liveness value.
49
+ "liveness" text NOT NULL DEFAULT 'cold'
50
+ CHECK ("liveness" IN ('cold','warming','warm','draining')),
51
+
52
+ -- Derived refcount = COUNT(sandbox_lease_holders for this lease). Stored
53
+ -- denormalized for fast branch decisions, but every mutation recomputes it
54
+ -- from the holder rows (holders are the source of truth; this is a cache).
55
+ "refcount" integer NOT NULL DEFAULT 0 CHECK ("refcount" >= 0),
56
+
57
+ -- Split counts. turn_holders is TTL-EXEMPT (released only by the Temporal
58
+ -- activity lifecycle / worker-death requeue, never reaped). viewer_holders is
59
+ -- TTL-reapable. The warm->draining CAS is guarded AND turn_holders = 0.
60
+ "turn_holders" integer NOT NULL DEFAULT 0 CHECK ("turn_holders" >= 0),
61
+ "viewer_holders" integer NOT NULL DEFAULT 0 CHECK ("viewer_holders" >= 0),
62
+
63
+ -- Provider identity of the live box + how to reach it.
64
+ "instance_id" text, -- provider sandbox id (NULL while cold)
65
+ "backend" text NOT NULL, -- 'modal'|'daytona'|... (sessions.sandbox_backend copy)
66
+ "os" text NOT NULL DEFAULT 'linux',
67
+ "data_plane_url" text, -- current scoped Channel-B (VNC-WS) URL,
68
+ -- recorded by any worker via an
69
+ -- event-driven resolveExposedPort under
70
+ -- the epoch fence (no owner process).
71
+
72
+ -- THE FENCE. Monotonic, bumped on every warming->warm commit / re-establish.
73
+ -- A stale re-dispatched writer fails its CAS and backs off when its cached
74
+ -- epoch != row epoch. integer (NOT bigint) — see header.
75
+ "lease_epoch" integer NOT NULL DEFAULT 0,
76
+
77
+ -- THE GROUP BOX-ENVELOPE (the "envelope split" Critical). resume_backend_id +
78
+ -- resume_state are the small recovery descriptor needed to resume()-by-id the
79
+ -- group's box (warm reattach or cold restore from snapshot) without a
80
+ -- per-session DB join. Folded onto the lease so the group has ONE envelope.
81
+ "resume_backend_id" text,
82
+ "resume_state" jsonb,
83
+
84
+ -- Warm-time billing cursor (the meter the stateless ticks advance in P2.1):
85
+ -- * last_meter_at = the accrual cursor (when warm-seconds were last metered);
86
+ -- * last_meter_tick = the idempotency tick; warm_seconds is accrued idempotent
87
+ -- on (sandbox_group_id, lease_epoch, last_meter_tick) so a retried/duplicate
88
+ -- tick never double-counts.
89
+ "last_meter_at" timestamptz,
90
+ "last_meter_tick" integer NOT NULL DEFAULT 0,
91
+
92
+ -- Heartbeat-TTL of the LEASE itself: refreshed on acquire, on the turn's 30s
93
+ -- activity heartbeat, and on a viewer's app-level API heartbeat. A 'warming'
94
+ -- row whose expires_at lapses = an uncaught spawner death -> reaper resets to
95
+ -- 'cold' (warming-death == lapse, single expires_at).
96
+ "expires_at" timestamptz NOT NULL,
97
+
98
+ "created_at" timestamptz NOT NULL DEFAULT now(),
99
+ "updated_at" timestamptz NOT NULL DEFAULT now()
100
+ );
101
+
102
+ -- THE SINGLETON GUARANTEE. One lease row per (workspace, group); INSERT ON
103
+ -- CONFLICT DO NOTHING + this index is what makes acquireLease idempotent under
104
+ -- a race.
105
+ CREATE UNIQUE INDEX IF NOT EXISTS "sandbox_leases_group_idx"
106
+ ON "sandbox_leases" ("workspace_id", "sandbox_group_id");
107
+
108
+ -- Reaper scan index: find lapsed leases cheaply. Partial on the three states the
109
+ -- reaper sweep acts on (warming-TTL-expired -> cold; warm-idle -> draining;
110
+ -- draining-grace-elapsed -> drainable). The SECURITY-DEFINER sweep predicate
111
+ -- rides this index.
112
+ CREATE INDEX IF NOT EXISTS "sandbox_leases_reaper_idx"
113
+ ON "sandbox_leases" ("expires_at")
114
+ WHERE "liveness" IN ('warming','warm','draining');
115
+
116
+ -- ============== sandbox_lease_holders (one row per holder) ===================
117
+ -- Release is delete-my-row, never a blind refcount--. A retried release
118
+ -- (Temporal at-least-once) deleting an already-gone row is a clean no-op.
119
+ CREATE TABLE IF NOT EXISTS "sandbox_lease_holders" (
120
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
121
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE CASCADE,
122
+ "workspace_id" uuid NOT NULL REFERENCES "workspaces"("id") ON DELETE CASCADE,
123
+ "lease_id" uuid NOT NULL REFERENCES "sandbox_leases"("id") ON DELETE CASCADE,
124
+
125
+ "kind" text NOT NULL CHECK ("kind" IN ('turn','viewer')),
126
+
127
+ -- Stable per-holder identity:
128
+ -- turn -> the session_turns.id (one turn = one holder; turns are sequential)
129
+ -- viewer -> the access-grant-scoped viewer connection id minted at handshake
130
+ -- Carrying holder_id makes acquire idempotent: a duplicate acquire for the
131
+ -- same (lease,kind,holder) is ON CONFLICT DO UPDATE (heartbeat refresh), not a
132
+ -- double-increment.
133
+ "holder_id" text NOT NULL,
134
+
135
+ -- The attributing session within the (possibly shared) group: which session
136
+ -- this holder belongs to (attribution/disclosure for shared boxes). The lease
137
+ -- is group-keyed; the holder records the session.
138
+ "subject_id" uuid,
139
+
140
+ -- Last app-level heartbeat. turn holders refresh via the 30s activity
141
+ -- heartbeat; viewer holders via the client->server Channel-A viewer ping.
142
+ -- reapStaleLeaseHolders deletes viewer rows older than viewerHolderTTL.
143
+ "last_heartbeat_at" timestamptz NOT NULL DEFAULT now(),
144
+ "created_at" timestamptz NOT NULL DEFAULT now()
145
+ );
146
+
147
+ -- Idempotency key for a holder: a given (lease, kind, holder) exists at most
148
+ -- once. acquireLease inserts ON CONFLICT DO UPDATE (refresh heartbeat) so a
149
+ -- reconnecting viewer or a retried acquire never double-counts.
150
+ CREATE UNIQUE INDEX IF NOT EXISTS "sandbox_lease_holders_holder_idx"
151
+ ON "sandbox_lease_holders" ("lease_id", "kind", "holder_id");
152
+
153
+ -- Reaper scan: viewer holders by staleness.
154
+ CREATE INDEX IF NOT EXISTS "sandbox_lease_holders_stale_idx"
155
+ ON "sandbox_lease_holders" ("kind", "last_heartbeat_at");
156
+
157
+ -- Recompute-refcount join.
158
+ CREATE INDEX IF NOT EXISTS "sandbox_lease_holders_lease_idx"
159
+ ON "sandbox_lease_holders" ("lease_id");
160
+
161
+ -- ============== RLS + grants (verbatim 0005/0007 boilerplate) ================
162
+ ALTER TABLE "sandbox_leases" ENABLE ROW LEVEL SECURITY;
163
+ ALTER TABLE "sandbox_leases" FORCE ROW LEVEL SECURITY;
164
+ ALTER TABLE "sandbox_lease_holders" ENABLE ROW LEVEL SECURITY;
165
+ ALTER TABLE "sandbox_lease_holders" FORCE ROW LEVEL SECURITY;
166
+
167
+ DO $$
168
+ BEGIN
169
+ IF EXISTS (
170
+ SELECT 1 FROM pg_policies
171
+ WHERE schemaname = current_schema() AND tablename = 'sandbox_leases' AND policyname = 'workspace_isolation'
172
+ ) THEN
173
+ DROP POLICY workspace_isolation ON "sandbox_leases";
174
+ END IF;
175
+ END $$;
176
+ CREATE POLICY workspace_isolation ON "sandbox_leases"
177
+ USING (opengeni_private.workspace_rls_visible(account_id, workspace_id))
178
+ WITH CHECK (opengeni_private.workspace_rls_visible(account_id, workspace_id));
179
+
180
+ DO $$
181
+ BEGIN
182
+ IF EXISTS (
183
+ SELECT 1 FROM pg_policies
184
+ WHERE schemaname = current_schema() AND tablename = 'sandbox_lease_holders' AND policyname = 'workspace_isolation'
185
+ ) THEN
186
+ DROP POLICY workspace_isolation ON "sandbox_lease_holders";
187
+ END IF;
188
+ END $$;
189
+ CREATE POLICY workspace_isolation ON "sandbox_lease_holders"
190
+ USING (opengeni_private.workspace_rls_visible(account_id, workspace_id))
191
+ WITH CHECK (opengeni_private.workspace_rls_visible(account_id, workspace_id));
192
+
193
+ -- ============== The SECURITY-DEFINER cross-workspace reaper sweep (OD-3) =====
194
+ -- The reaper Temporal Schedule (P1.3) runs ONE global sweep, not a per-workspace
195
+ -- loop: it must see stale rows across ALL workspaces in a single pass. Under
196
+ -- FORCE RLS the workspace_isolation policy would hide every other workspace's
197
+ -- rows from a scoped connection, so the sweep is a SECURITY DEFINER function
198
+ -- owned by the migration role (which owns the tables and thus is not subject to
199
+ -- their RLS when it runs as DEFINER) — the one sanctioned cross-workspace read.
200
+ --
201
+ -- DB-ONLY: this function does NOT call any provider. It mutates the lease rows
202
+ -- (TTL-reap stale viewer holders, reset warming-death to cold, recompute
203
+ -- refcounts, enter draining at refcount 0) and RETURNS the (workspace_id,
204
+ -- sandbox_group_id) of leases whose drain grace has elapsed so the caller can
205
+ -- issue the provider stop() (that stop() is P1.3's runtime concern, not the
206
+ -- DB's). One predicate-driven pass over the reaper index.
207
+ CREATE OR REPLACE FUNCTION opengeni_private.reap_sandbox_leases(
208
+ p_viewer_holder_ttl_ms bigint,
209
+ p_idle_grace_ms bigint
210
+ )
211
+ RETURNS TABLE (workspace_id uuid, sandbox_group_id uuid, instance_id text, lease_epoch integer)
212
+ LANGUAGE plpgsql
213
+ SECURITY DEFINER
214
+ -- EMBED-SAFE: this function references sandbox_leases / sandbox_lease_holders
215
+ -- (the DATA schema) and opengeni_private helpers. It deliberately does NOT pin
216
+ -- `SET search_path = public, ...` because under the embedded dedicated-schema
217
+ -- topology those tables live in the host's chosen schema (e.g. `opengeni`), not
218
+ -- `public`; a pinned `public` path would silently resolve them to an empty/
219
+ -- absent table and the reaper sweep would no-op. Inheriting the caller's
220
+ -- search_path resolves the data tables in standalone (caller path `public,
221
+ -- opengeni_private`) AND embedded (caller path `<schema>, opengeni_private`)
222
+ -- alike. opengeni_private fns are still called with an absolute prefix below.
223
+ AS $$
224
+ BEGIN
225
+ -- (a) Reap stale VIEWER holders cross-workspace (turn holders are TTL-exempt).
226
+ DELETE FROM sandbox_lease_holders h
227
+ WHERE h.kind = 'viewer'
228
+ AND h.last_heartbeat_at < now() - make_interval(secs => p_viewer_holder_ttl_ms / 1000.0);
229
+
230
+ -- (b) Recompute refcounts from the holder rows for every lease; warm leases
231
+ -- that hit 0 (AND turn_holders = 0) enter draining with a fresh grace
232
+ -- deadline (idleGraceMs, the SAME horizon releaseLeaseHolder stamps).
233
+ UPDATE sandbox_leases L SET
234
+ refcount = c.total,
235
+ turn_holders = c.turns,
236
+ viewer_holders = c.viewers,
237
+ liveness = CASE WHEN L.liveness = 'warm' AND c.total = 0 AND c.turns = 0
238
+ THEN 'draining' ELSE L.liveness END,
239
+ expires_at = CASE WHEN L.liveness = 'warm' AND c.total = 0 AND c.turns = 0
240
+ THEN now() + make_interval(secs => p_idle_grace_ms / 1000.0)
241
+ ELSE L.expires_at END,
242
+ updated_at = now()
243
+ FROM (
244
+ SELECT L2.id,
245
+ (SELECT count(*) FROM sandbox_lease_holders h WHERE h.lease_id = L2.id)::int AS total,
246
+ (SELECT count(*) FROM sandbox_lease_holders h WHERE h.lease_id = L2.id AND h.kind = 'turn')::int AS turns,
247
+ (SELECT count(*) FROM sandbox_lease_holders h WHERE h.lease_id = L2.id AND h.kind = 'viewer')::int AS viewers
248
+ FROM sandbox_leases L2
249
+ ) c
250
+ WHERE L.id = c.id;
251
+
252
+ -- (c) WARMING-death: a 'warming' row whose LEASE TTL lapsed = the spawner
253
+ -- process died mid-resume. Reset to cold so a queued turn can re-acquire
254
+ -- and re-spawn. instance_id/data_plane_url cleared (a warming row may have
255
+ -- a stale handle from a prior epoch).
256
+ UPDATE sandbox_leases SET
257
+ liveness = 'cold', instance_id = NULL,
258
+ resume_backend_id = NULL, resume_state = NULL,
259
+ data_plane_url = NULL, updated_at = now()
260
+ WHERE liveness = 'warming' AND expires_at < now();
261
+
262
+ -- (d) DRAINING-grace elapsed: surface the (workspace, group) of every lease
263
+ -- whose grace is up AND still idle, with instance_id + epoch, so the
264
+ -- caller (P1.3) can issue the provider stop() then confirmDrainCold-CAS
265
+ -- draining->cold under the epoch fence. DB-only: no provider call here.
266
+ RETURN QUERY
267
+ SELECT L.workspace_id, L.sandbox_group_id, L.instance_id, L.lease_epoch
268
+ FROM sandbox_leases L
269
+ WHERE L.liveness = 'draining' AND L.expires_at < now() AND L.refcount = 0;
270
+ END;
271
+ $$;
272
+
273
+ -- ============================================================================
274
+ -- Warm-meter read (P2.1) — the reaper-tick metering input.
275
+ --
276
+ -- The reaper sweep is the warm-meter tick for VIEWER-ONLY boxes between turns (a
277
+ -- turn-held box is metered by the turn's own activity heartbeat, so we EXCLUDE
278
+ -- turn_holders > 0 here to avoid double-metering). Like the reaper sweep this is
279
+ -- a cross-workspace read that FORCE RLS would hide from a scoped connection, so
280
+ -- it is a SECURITY DEFINER read fn (DB-only, no mutation — the worker calls
281
+ -- accrueWarmSeconds per row, which does the epoch-fenced cursor advance + the
282
+ -- (group, epoch, tick)-idempotent usage insert under the lease row lock).
283
+ --
284
+ -- Returns ONE row per WARM group with no turn holders (a singleton group is one
285
+ -- box → one row → one warm-seconds stream regardless of N viewer sessions).
286
+ CREATE OR REPLACE FUNCTION opengeni_private.list_meterable_warm_leases()
287
+ RETURNS TABLE (
288
+ account_id uuid,
289
+ workspace_id uuid,
290
+ sandbox_group_id uuid,
291
+ lease_epoch integer,
292
+ backend text
293
+ )
294
+ LANGUAGE sql
295
+ SECURITY DEFINER
296
+ -- EMBED-SAFE: see reap_sandbox_leases above — no pinned `public` search_path so
297
+ -- the cross-workspace warm-lease read resolves sandbox_leases in whatever data
298
+ -- schema the caller's search_path selects (standalone `public`, embedded
299
+ -- `<schema>`). Inherits the caller's path instead of hardcoding `public`.
300
+ AS $$
301
+ SELECT L.account_id, L.workspace_id, L.sandbox_group_id, L.lease_epoch, L.backend
302
+ FROM sandbox_leases L
303
+ WHERE L.liveness = 'warm' AND L.turn_holders = 0;
304
+ $$;
305
+
306
+ DO $$
307
+ BEGIN
308
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
309
+ GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO opengeni_app;
310
+ GRANT EXECUTE ON FUNCTION opengeni_private.reap_sandbox_leases(bigint, bigint) TO opengeni_app;
311
+ GRANT EXECUTE ON FUNCTION opengeni_private.list_meterable_warm_leases() TO opengeni_app;
312
+ END IF;
313
+ END $$;
@@ -0,0 +1,89 @@
1
+ -- Per-session sandbox OS + the shared-sandbox group identity.
2
+ --
3
+ -- Two forward-only, behavior-preserving column adds (P0.5; design-of-record
4
+ -- 08-implementation-plan.md P0.5 + 05-addendum-shared-sandboxes.md B.1):
5
+ --
6
+ -- (a) OS axis. sessions.sandbox_os carries the OS the session's box runs
7
+ -- (default 'linux' — today's only OS, so every existing + new row is
8
+ -- 'linux' with no behavior change). session_turns.sandbox_os is a NULLable
9
+ -- per-turn override (NULL = inherit the session's OS). Both CHECK-constrained
10
+ -- to the SandboxOs enum (linux|macos|windows). The OS is also stamped into
11
+ -- the recovery-envelope JSON so re-establish needs no DB join.
12
+ --
13
+ -- (b) Shared-sandbox group. sessions.sandbox_group_id is the BOX's identity:
14
+ -- every session that founds its own box has sandbox_group_id == id (a
15
+ -- singleton group, group === session), so today's 1:1 world is a
16
+ -- behavior-preserving no-op. A session spawned shared inherits its parent's
17
+ -- sandbox_group_id so both run in ONE box. The live lease row (0017), not a
18
+ -- sandbox_groups table, materializes "this group has a box."
19
+ --
20
+ -- DELIBERATELY NOT an FK to sessions(id): the value is either this row's own
21
+ -- id or an ANCESTOR session's id in the same workspace. An FK would let a
22
+ -- founder's deletion cascade-kill a box still in use by a spawned session
23
+ -- (addendum stress b.1). Do not "tidy" this into an FK.
24
+ --
25
+ -- The value cannot SQL-default to id (id is gen_random_uuid(), unknown at
26
+ -- default-eval time) — the app generates one uuid and uses it for both id
27
+ -- and sandbox_group_id in a single insert. The column is added NULLable for
28
+ -- the backfill, populated (= id for every existing row), then SET NOT NULL.
29
+
30
+ -- (a) OS axis ---------------------------------------------------------------
31
+
32
+ ALTER TABLE "sessions"
33
+ ADD COLUMN IF NOT EXISTS "sandbox_os" text NOT NULL DEFAULT 'linux';
34
+
35
+ DO $$
36
+ BEGIN
37
+ IF NOT EXISTS (
38
+ SELECT 1 FROM pg_constraint WHERE conname = 'sessions_sandbox_os_check'
39
+ ) THEN
40
+ ALTER TABLE "sessions"
41
+ ADD CONSTRAINT "sessions_sandbox_os_check"
42
+ CHECK ("sandbox_os" IN ('linux', 'macos', 'windows'));
43
+ END IF;
44
+ END $$;
45
+
46
+ ALTER TABLE "session_turns"
47
+ ADD COLUMN IF NOT EXISTS "sandbox_os" text;
48
+
49
+ DO $$
50
+ BEGIN
51
+ IF NOT EXISTS (
52
+ SELECT 1 FROM pg_constraint WHERE conname = 'session_turns_sandbox_os_check'
53
+ ) THEN
54
+ ALTER TABLE "session_turns"
55
+ ADD CONSTRAINT "session_turns_sandbox_os_check"
56
+ CHECK ("sandbox_os" IS NULL OR "sandbox_os" IN ('linux', 'macos', 'windows'));
57
+ END IF;
58
+ END $$;
59
+
60
+ -- (b) Shared-sandbox group identity -----------------------------------------
61
+
62
+ -- 1. Add NULLable transiently for the backfill.
63
+ ALTER TABLE "sessions"
64
+ ADD COLUMN IF NOT EXISTS "sandbox_group_id" uuid;
65
+
66
+ -- 2. Backfill: every existing session is its OWN singleton group (group === id).
67
+ -- This is the behavior-preserving identity that makes the whole change a
68
+ -- no-op for today's 1:1 world.
69
+ UPDATE "sessions" SET "sandbox_group_id" = "id" WHERE "sandbox_group_id" IS NULL;
70
+
71
+ -- 3. Enforce NOT NULL (the app supplies the value on every insert from here on).
72
+ ALTER TABLE "sessions"
73
+ ALTER COLUMN "sandbox_group_id" SET NOT NULL;
74
+
75
+ -- 4. Routing index: resolve session_id -> sandbox_group_id at every lease entry
76
+ -- point (turn resume-by-id, viewer attach) and enumerate "all sessions in a
77
+ -- group" for attribution/disclosure. Workspace-scoped (the workspace is the
78
+ -- access boundary; the group uuid is not).
79
+ CREATE INDEX IF NOT EXISTS "sessions_sandbox_group_idx"
80
+ ON "sessions" ("workspace_id", "sandbox_group_id");
81
+
82
+ -- Re-grant on the new columns (idempotent; mirrors the boilerplate in earlier
83
+ -- migrations so a fresh opengeni_app role can read/write the added columns).
84
+ DO $$
85
+ BEGIN
86
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
87
+ GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO opengeni_app;
88
+ END IF;
89
+ END $$;
@@ -0,0 +1,94 @@
1
+ -- The un-redacted-pixel consent gate (P3.2; design-of-record
2
+ -- 08-implementation-plan.md P3.2 + modules/07-channel-b.md §1.1/§6 +
3
+ -- 05-addendum-shared-sandboxes.md E.1).
4
+ --
5
+ -- A row here records that a PRINCIPAL (subject) explicitly acknowledged the
6
+ -- un-redacted desktop pixel plane for a (workspace, group, subject) — and, when
7
+ -- the box is shared with sibling sessions, that they also consented to the
8
+ -- SHARED-EXPOSURE disclosure (watching A's desktop also shows B's agent on the
9
+ -- one framebuffer; the pixels cannot be redacted, so consent — not authz — is
10
+ -- the gate, addendum E.1 / stress g).
11
+ --
12
+ -- The acknowledgment is keyed on the GROUP, not the session: the un-redacted
13
+ -- surface is the BOX's :0 framebuffer (one per group), so acknowledging it once
14
+ -- for a group covers every conversation in that group. The shared bit is
15
+ -- recorded so a later transition from solo→shared can re-require consent if the
16
+ -- product wants it (today the negotiation reports `shared` from the live group
17
+ -- session-set and the gate keys on the recorded `acknowledged_shared`).
18
+ --
19
+ -- Reuses the acknowledgment machinery from P3.1 — NO new permission beyond
20
+ -- stream:acknowledge (the principal must hold stream:acknowledge to write a row,
21
+ -- and stream:view to use the desktop path the row gates).
22
+ --
23
+ -- DDL is INERT until the routes wire it (flag sandboxOwnershipEnabled); this
24
+ -- migration only creates the table, indexes, RLS, and grants. Forward-only,
25
+ -- behavior-preserving: no existing row references it.
26
+
27
+ -- ============== session_stream_acknowledgments (one row per consent) =========
28
+ -- The natural key is (workspace_id, sandbox_group_id, subject_id): a principal's
29
+ -- consent to the group's un-redacted pixel plane. acknowledged_shared records
30
+ -- whether the shared-exposure disclosure was also consented (the gate returns
31
+ -- 409 shared_acknowledgment_required until this is true for a shared box).
32
+ CREATE TABLE IF NOT EXISTS "session_stream_acknowledgments" (
33
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
34
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE CASCADE,
35
+ "workspace_id" uuid NOT NULL REFERENCES "workspaces"("id") ON DELETE CASCADE,
36
+
37
+ -- The GROUP whose un-redacted :0 framebuffer this consent covers. A bare uuid
38
+ -- (NOT an FK), matching sandbox_leases.sandbox_group_id: the value is a session
39
+ -- id or an ancestor's id in the same workspace; the lease row, not a
40
+ -- sandbox_groups table, materializes the group (addendum B.1).
41
+ "sandbox_group_id" uuid NOT NULL,
42
+
43
+ -- The acknowledging PRINCIPAL (the access-grant subjectId). Consent is
44
+ -- per-principal: A acknowledging does not consent on B's behalf.
45
+ "subject_id" text NOT NULL,
46
+
47
+ -- Always true when a row exists (the un-redacted pixel plane was acknowledged);
48
+ -- carried explicitly so the column self-documents and a future "withdraw"
49
+ -- toggles it without deleting attribution.
50
+ "acknowledged_unredacted" boolean NOT NULL DEFAULT true,
51
+
52
+ -- Whether the SHARED-EXPOSURE disclosure was also consented (addendum E.1).
53
+ -- The gate returns 409 shared_acknowledgment_required for a shared box until
54
+ -- this is true; for a solo box it is irrelevant (the un-redacted ack suffices).
55
+ "acknowledged_shared" boolean NOT NULL DEFAULT false,
56
+
57
+ "acknowledged_at" timestamptz NOT NULL DEFAULT now(),
58
+ "created_at" timestamptz NOT NULL DEFAULT now(),
59
+ "updated_at" timestamptz NOT NULL DEFAULT now()
60
+ );
61
+
62
+ -- One consent row per (workspace, group, subject). Re-acknowledging (e.g. a
63
+ -- solo→shared upgrade flipping acknowledged_shared) is an ON CONFLICT DO UPDATE,
64
+ -- never a duplicate row.
65
+ CREATE UNIQUE INDEX IF NOT EXISTS "session_stream_ack_subject_idx"
66
+ ON "session_stream_acknowledgments" ("workspace_id", "sandbox_group_id", "subject_id");
67
+
68
+ -- Lookup-by-group (enumerate who has consented to a group's pixel plane).
69
+ CREATE INDEX IF NOT EXISTS "session_stream_ack_group_idx"
70
+ ON "session_stream_acknowledgments" ("workspace_id", "sandbox_group_id");
71
+
72
+ -- ============== RLS + grants (verbatim 0017 boilerplate) =====================
73
+ ALTER TABLE "session_stream_acknowledgments" ENABLE ROW LEVEL SECURITY;
74
+ ALTER TABLE "session_stream_acknowledgments" FORCE ROW LEVEL SECURITY;
75
+
76
+ DO $$
77
+ BEGIN
78
+ IF EXISTS (
79
+ SELECT 1 FROM pg_policies
80
+ WHERE schemaname = current_schema() AND tablename = 'session_stream_acknowledgments' AND policyname = 'workspace_isolation'
81
+ ) THEN
82
+ DROP POLICY workspace_isolation ON "session_stream_acknowledgments";
83
+ END IF;
84
+ END $$;
85
+ CREATE POLICY workspace_isolation ON "session_stream_acknowledgments"
86
+ USING (opengeni_private.workspace_rls_visible(account_id, workspace_id))
87
+ WITH CHECK (opengeni_private.workspace_rls_visible(account_id, workspace_id));
88
+
89
+ DO $$
90
+ BEGIN
91
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
92
+ GRANT SELECT, INSERT, UPDATE, DELETE ON "session_stream_acknowledgments" TO opengeni_app;
93
+ END IF;
94
+ END $$;
@@ -0,0 +1,88 @@
1
+ -- Session recordings — the durable index for the "agent films itself proving
2
+ -- the fix" loop (P4.3; design-of-record 08-implementation-plan.md P4.3 +
3
+ -- modules/05-computer-use.md §3.5).
4
+ --
5
+ -- One row per recording: an ffmpeg x11grab of the SAME :0 framebuffer humans
6
+ -- watch (Channel B), captured while the box is held warm, finalized by reading
7
+ -- the bytes off the box and PUTting them to @opengeni/storage IN THE SAME
8
+ -- process that holds the resumed-by-id handle (the turn activity for an on-turn
9
+ -- recording; the API in-process for an off-turn finalize) — the bytes are NEVER
10
+ -- a Temporal payload (F10). The signed-URL replay route reads `storage_key`.
11
+ --
12
+ -- The acknowledgment / un-redacted-pixel consent (P3.2) gates whether a viewer
13
+ -- may watch the LIVE desktop; a recording artifact is gated by sessions:read on
14
+ -- the per-fetch signed-URL route (the row carries no long-lived URL). Recordings
15
+ -- are un-redacted (the framebuffer may show creds on screen), so recordingEnabled
16
+ -- is an opt-in per deployment and the artifact inherits workspace RLS.
17
+ --
18
+ -- DDL is INERT until the worker/API wire it (flag sandboxDesktopEnabled); this
19
+ -- migration only creates the table, indexes, RLS, and grants. Forward-only,
20
+ -- behavior-preserving: no existing row references it.
21
+
22
+ -- ============== session_recordings (one row per recording) ===================
23
+ CREATE TABLE IF NOT EXISTS "session_recordings" (
24
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
25
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE CASCADE,
26
+ "workspace_id" uuid NOT NULL REFERENCES "workspaces"("id") ON DELETE CASCADE,
27
+ "session_id" uuid NOT NULL REFERENCES "sessions"("id") ON DELETE CASCADE,
28
+ -- The turn that started the recording (on-turn / on-verify modes). NULL for a
29
+ -- manual recording with no originating turn. ON DELETE SET NULL: a deleted
30
+ -- turn must not cascade-kill the recording artifact row.
31
+ "turn_id" uuid REFERENCES "session_turns"("id") ON DELETE SET NULL,
32
+
33
+ -- recording | finalizing | available | failed (the §3.1 state machine).
34
+ "state" text NOT NULL,
35
+ -- manual | on-turn | on-verify (who started it).
36
+ "mode" text NOT NULL,
37
+ -- h264-mp4 | vp9-webm (the container/codec).
38
+ "codec" text NOT NULL,
39
+
40
+ -- The @opengeni/storage object key (NULL until finalize commits `available`).
41
+ "storage_key" text,
42
+ "size_bytes" bigint,
43
+ "duration_seconds" double precision,
44
+
45
+ -- The framebuffer geometry the recording was captured at (must match the live
46
+ -- Xvfb geometry — the §7 config coupling).
47
+ "width" integer NOT NULL,
48
+ "height" integer NOT NULL,
49
+
50
+ -- The verification rationale (on-verify "reason") OR the failure reason/detail.
51
+ -- Agent-authored free text — capped + scrubbed by the producer before storage.
52
+ "reason" text,
53
+
54
+ "created_at" timestamptz NOT NULL DEFAULT now(),
55
+ "finalized_at" timestamptz,
56
+
57
+ CONSTRAINT "session_recordings_state_chk" CHECK ("state" IN ('recording','finalizing','available','failed')),
58
+ CONSTRAINT "session_recordings_mode_chk" CHECK ("mode" IN ('manual','on-turn','on-verify')),
59
+ CONSTRAINT "session_recordings_codec_chk" CHECK ("codec" IN ('h264-mp4','vp9-webm'))
60
+ );
61
+
62
+ -- List a session's recordings newest-first without scanning the event spine.
63
+ CREATE INDEX IF NOT EXISTS "session_recordings_session_idx"
64
+ ON "session_recordings" ("workspace_id", "session_id", "created_at" DESC);
65
+
66
+ -- ============== RLS + grants (verbatim 0017/0019 boilerplate) ================
67
+ ALTER TABLE "session_recordings" ENABLE ROW LEVEL SECURITY;
68
+ ALTER TABLE "session_recordings" FORCE ROW LEVEL SECURITY;
69
+
70
+ DO $$
71
+ BEGIN
72
+ IF EXISTS (
73
+ SELECT 1 FROM pg_policies
74
+ WHERE schemaname = current_schema() AND tablename = 'session_recordings' AND policyname = 'workspace_isolation'
75
+ ) THEN
76
+ DROP POLICY workspace_isolation ON "session_recordings";
77
+ END IF;
78
+ END $$;
79
+ CREATE POLICY workspace_isolation ON "session_recordings"
80
+ USING (opengeni_private.workspace_rls_visible(account_id, workspace_id))
81
+ WITH CHECK (opengeni_private.workspace_rls_visible(account_id, workspace_id));
82
+
83
+ DO $$
84
+ BEGIN
85
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
86
+ GRANT SELECT, INSERT, UPDATE, DELETE ON "session_recordings" TO opengeni_app;
87
+ END IF;
88
+ END $$;