@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.
- package/dist/chunk-57MLICFR.js +121 -0
- package/dist/chunk-57MLICFR.js.map +1 -0
- package/dist/chunk-OGCE6O2X.js +52 -0
- package/dist/chunk-OGCE6O2X.js.map +1 -0
- package/dist/chunk-PSX56ZTL.js +1093 -0
- package/dist/chunk-PSX56ZTL.js.map +1 -0
- package/dist/chunk-PZ5AY32C.js +10 -0
- package/dist/chunk-PZ5AY32C.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +5165 -0
- package/dist/index.js.map +1 -0
- package/dist/migrate.d.ts +40 -0
- package/dist/migrate.js +10 -0
- package/dist/migrate.js.map +1 -0
- package/dist/provision-roles.d.ts +2063 -0
- package/dist/provision-roles.js +8 -0
- package/dist/provision-roles.js.map +1 -0
- package/dist/schema-CaeZQAJQ.d.ts +9705 -0
- package/dist/schema.d.ts +3 -0
- package/dist/schema.js +110 -0
- package/dist/schema.js.map +1 -0
- package/drizzle/0000_initial.sql +179 -0
- package/drizzle/0001_workspace_auth_billing.sql +590 -0
- package/drizzle/0002_packs_and_social.sql +99 -0
- package/drizzle/0003_capability_catalog.sql +73 -0
- package/drizzle/0004_workspace_environments.sql +65 -0
- package/drizzle/0005_session_goals.sql +45 -0
- package/drizzle/0006_workspace_packs.sql +31 -0
- package/drizzle/0007_session_history_items.sql +66 -0
- package/drizzle/0008_session_first_party_mcp_permissions.sql +5 -0
- package/drizzle/0009_goal_sessions_first_party_goals_manage.sql +34 -0
- package/drizzle/0010_session_parent_linkage.sql +30 -0
- package/drizzle/0011_context_compaction.sql +33 -0
- package/drizzle/0012_compaction_summary_fractional_position.sql +19 -0
- package/drizzle/0013_session_compact_requested.sql +16 -0
- package/drizzle/0014_repair_orphaned_function_call_results.sql +125 -0
- package/drizzle/0015_workspace_agent_instructions.sql +17 -0
- package/drizzle/0016_session_create_idempotency.sql +27 -0
- package/drizzle/0017_sandbox_leases.sql +313 -0
- package/drizzle/0018_sandbox_os.sql +89 -0
- package/drizzle/0019_session_stream_acknowledgments.sql +94 -0
- package/drizzle/0020_session_recordings.sql +88 -0
- package/drizzle/0021_sandbox_pty_sessions.sql +70 -0
- package/drizzle/0022_sandbox_lease_terminal_url.sql +32 -0
- package/drizzle/0023_session_title.sql +19 -0
- package/drizzle/0024_codex_subscription_credentials.sql +51 -0
- package/drizzle/0024_sandboxes_enrollments_metrics.sql +262 -0
- package/drizzle/0025_device_enrollment_requests.sql +142 -0
- package/drizzle/0026_device_enrollment_user_code_resolver.sql +47 -0
- package/drizzle/0027_session_working_dir.sql +24 -0
- package/drizzle/0028_codex_multi_account.sql +85 -0
- package/drizzle/0029_session_history_item_producer.sql +31 -0
- package/drizzle/0030_agent_run_state_frozen_codex.sql +35 -0
- package/drizzle/0031_codex_usage_cache.sql +21 -0
- package/drizzle/0032_codex_account_cooldown.sql +18 -0
- package/drizzle/0033_codex_connector_cache.sql +20 -0
- package/drizzle/0034_sandbox_lease_image.sql +21 -0
- package/drizzle/meta/_journal.json +167 -0
- package/package.json +66 -0
- package/src/codex-token-resolver.ts +247 -0
- package/src/environment-crypto.ts +51 -0
- package/src/event-payload-sanitizer.ts +89 -0
- package/src/index.ts +7776 -0
- package/src/migrate.ts +95 -0
- package/src/provision-roles.ts +198 -0
- 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 $$;
|