@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,70 @@
1
+ -- Channel-A interactive PTY sessions — the ONLY new persistent state the
2
+ -- structured-services surface needs (P4.4; design-of-record
3
+ -- modules/08-channel-a.md §3.1).
4
+ --
5
+ -- FS list/read/write/search and Git status/diff/log/show are STATELESS point
6
+ -- queries against the live box (resume-by-id, run, drop) — they persist nothing;
7
+ -- their notifications (fs.changed/git.changed) ride the existing session_events
8
+ -- log. An interactive PTY, by contrast, is a live in-box process: we map our
9
+ -- UUID ptyId <-> the SDK's numeric exec-session id (writeStdin({ sessionId })),
10
+ -- the owning workspace/session, the lease_epoch that fences the PTY to the box
11
+ -- it was opened on, and a last_input_at heartbeat so the one global reaper can
12
+ -- kill idle PTYs (TTL) and PTYs stranded by a box re-key (epoch mismatch ->
13
+ -- terminal.pty.exited{reason:"owner_gone"}).
14
+ --
15
+ -- DDL is INERT until the API wires the PTY routes (gated behind
16
+ -- sandboxOwnershipEnabled). Forward-only, behavior-preserving: no existing row
17
+ -- references it.
18
+
19
+ CREATE TABLE IF NOT EXISTS "sandbox_pty_sessions" (
20
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, -- == ptyId on the wire
21
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE CASCADE,
22
+ "workspace_id" uuid NOT NULL REFERENCES "workspaces"("id") ON DELETE CASCADE,
23
+ "session_id" uuid NOT NULL REFERENCES "sessions"("id") ON DELETE CASCADE,
24
+ -- The SDK numeric exec-session id used by writeStdin({ sessionId }). NULL until
25
+ -- the open exec yields a still-running process (a fast-exiting shell has none).
26
+ "exec_session_id" integer,
27
+ "lease_epoch" integer NOT NULL, -- fenced to the box that opened it
28
+ "cols" integer NOT NULL,
29
+ "rows" integer NOT NULL,
30
+ "shell" text NOT NULL,
31
+ "cwd" text NOT NULL,
32
+ "status" text NOT NULL DEFAULT 'open', -- 'open' | 'closed'
33
+ -- The viewer grant/subject that opened it (free-text — access subjects are not
34
+ -- always UUIDs, so a text column, never a uuid NOT NULL).
35
+ "opened_by" text NOT NULL,
36
+ "last_input_at" timestamptz NOT NULL DEFAULT now(),
37
+ "created_at" timestamptz NOT NULL DEFAULT now(),
38
+ "closed_at" timestamptz,
39
+
40
+ CONSTRAINT "sandbox_pty_sessions_status_chk" CHECK ("status" IN ('open','closed'))
41
+ );
42
+
43
+ -- List a session's OPEN PTYs (for reattach + reap) without scanning closed rows.
44
+ CREATE INDEX IF NOT EXISTS "sandbox_pty_sessions_session_idx"
45
+ ON "sandbox_pty_sessions" ("workspace_id", "session_id")
46
+ WHERE "status" = 'open';
47
+
48
+ -- ============== RLS + grants (verbatim 0017/0019/0020 boilerplate) ===========
49
+ ALTER TABLE "sandbox_pty_sessions" ENABLE ROW LEVEL SECURITY;
50
+ ALTER TABLE "sandbox_pty_sessions" FORCE ROW LEVEL SECURITY;
51
+
52
+ DO $$
53
+ BEGIN
54
+ IF EXISTS (
55
+ SELECT 1 FROM pg_policies
56
+ WHERE schemaname = current_schema() AND tablename = 'sandbox_pty_sessions' AND policyname = 'workspace_isolation'
57
+ ) THEN
58
+ DROP POLICY workspace_isolation ON "sandbox_pty_sessions";
59
+ END IF;
60
+ END $$;
61
+ CREATE POLICY workspace_isolation ON "sandbox_pty_sessions"
62
+ USING (opengeni_private.workspace_rls_visible(account_id, workspace_id))
63
+ WITH CHECK (opengeni_private.workspace_rls_visible(account_id, workspace_id));
64
+
65
+ DO $$
66
+ BEGIN
67
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
68
+ GRANT SELECT, INSERT, UPDATE, DELETE ON "sandbox_pty_sessions" TO opengeni_app;
69
+ END IF;
70
+ END $$;
@@ -0,0 +1,32 @@
1
+ -- The REAL PTY terminal (ttyd pty-ws) data-plane URL cache (P5.t).
2
+ --
3
+ -- The interactive terminal is now a REAL PTY streamed over the SAME Modal raw-TLS
4
+ -- tunnel the desktop noVNC uses (symmetric with Channel-B), replacing the broken
5
+ -- stateless ptyWrite-over-HTTP path (Modal kept the live PTY process handle in an
6
+ -- in-memory per-call activeProcesses Map; a fresh session per HTTP call hit
7
+ -- "session not found"). ttyd listens on 7681 in-box and is exposed over a SEPARATE
8
+ -- provider tunnel from the 6080 desktop noVNC — a DIFFERENT URL.
9
+ --
10
+ -- The lease already caches the single desktop tunnel URL (data_plane_url, the 6080
11
+ -- noVNC plane). The terminal tunnel (7681) resolves to its own URL, so it needs
12
+ -- its own cache column. mintTerminalStream records it here under the epoch fence
13
+ -- (recordLeaseTerminalDataPlaneUrl); the fast-path then re-mints only a fresh
14
+ -- scoped token against the cached URL (no box touch). It is reset to NULL on every
15
+ -- box re-key (commitWarmingToWarm / failWarmingToCold / drain-cold / warming-death
16
+ -- reset) exactly like data_plane_url, so a stale URL never survives a rollover.
17
+ --
18
+ -- Forward-only, behavior-preserving: NULLable, no backfill needed (a warm lease
19
+ -- re-resolves + records it on the next terminal attach; a cold/legacy row reads
20
+ -- NULL = "not yet minted").
21
+
22
+ ALTER TABLE "sandbox_leases"
23
+ ADD COLUMN IF NOT EXISTS "terminal_data_plane_url" text;
24
+
25
+ -- Re-grant on the new column (idempotent; mirrors the boilerplate in 0018 so a
26
+ -- fresh opengeni_app role can read/write the added column).
27
+ DO $$
28
+ BEGIN
29
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
30
+ GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO opengeni_app;
31
+ END IF;
32
+ END $$;
@@ -0,0 +1,19 @@
1
+ -- Agent-generated session display title.
2
+ --
3
+ -- A session gets a short title generated by the agent itself (via the
4
+ -- set_session_title first-party tool) — not a separate LLM call. `title` is
5
+ -- NULL until set, in which case the UI keeps falling back to the
6
+ -- initialMessage-derived label. `title_source` records who set it ('agent' for
7
+ -- auto/agent writes, 'user' for a manual rename); a user-set title is permanent
8
+ -- and the clobber guard (title_source IS DISTINCT FROM 'user') protects it from
9
+ -- later agent writes. Both nullable, no backfill.
10
+ ALTER TABLE "sessions"
11
+ ADD COLUMN IF NOT EXISTS "title" text,
12
+ ADD COLUMN IF NOT EXISTS "title_source" text;
13
+
14
+ DO $$
15
+ BEGIN
16
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
17
+ GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO opengeni_app;
18
+ END IF;
19
+ END $$;
@@ -0,0 +1,51 @@
1
+ -- Per-workspace ChatGPT/Codex subscription credentials.
2
+ --
3
+ -- One row per workspace. The secrets (access/refresh/id token) live INSIDE
4
+ -- credential_encrypted using the v1 AES-256-GCM envelope (environment-crypto.ts),
5
+ -- key = OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY — the same envelope used for
6
+ -- workspace environment variables. chatgpt_account_id / plan_type / scopes /
7
+ -- status are plaintext (request header + UI, non-secret). RLS enforces strict
8
+ -- per-workspace isolation, identical to workspace_environment_variables.
9
+
10
+ CREATE TABLE IF NOT EXISTS "codex_subscription_credentials" (
11
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
12
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE CASCADE,
13
+ "workspace_id" uuid NOT NULL REFERENCES "workspaces"("id") ON DELETE CASCADE,
14
+ "credential_encrypted" text NOT NULL,
15
+ "chatgpt_account_id" text,
16
+ "scopes" text,
17
+ "plan_type" text,
18
+ "is_fedramp" boolean NOT NULL DEFAULT false,
19
+ "expires_at" timestamptz,
20
+ "last_refresh_at" timestamptz,
21
+ "status" text NOT NULL DEFAULT 'active',
22
+ "last_error" text,
23
+ "version" integer NOT NULL DEFAULT 1,
24
+ "created_at" timestamptz NOT NULL DEFAULT now(),
25
+ "updated_at" timestamptz NOT NULL DEFAULT now()
26
+ );
27
+
28
+ CREATE UNIQUE INDEX IF NOT EXISTS "codex_subscription_credentials_workspace_idx"
29
+ ON "codex_subscription_credentials" ("workspace_id");
30
+
31
+ ALTER TABLE "codex_subscription_credentials" ENABLE ROW LEVEL SECURITY;
32
+ ALTER TABLE "codex_subscription_credentials" FORCE ROW LEVEL SECURITY;
33
+ DO $$
34
+ BEGIN
35
+ IF EXISTS (
36
+ SELECT 1 FROM pg_policies
37
+ WHERE schemaname = current_schema() AND tablename = 'codex_subscription_credentials' AND policyname = 'workspace_isolation'
38
+ ) THEN
39
+ DROP POLICY workspace_isolation ON "codex_subscription_credentials";
40
+ END IF;
41
+ END $$;
42
+ CREATE POLICY workspace_isolation ON "codex_subscription_credentials"
43
+ USING (opengeni_private.workspace_rls_visible(account_id, workspace_id))
44
+ WITH CHECK (opengeni_private.workspace_rls_visible(account_id, workspace_id));
45
+
46
+ DO $$
47
+ BEGIN
48
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
49
+ GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO opengeni_app;
50
+ END IF;
51
+ END $$;
@@ -0,0 +1,262 @@
1
+ -- First-class swappable sandboxes + enrollment records + per-machine metrics, and
2
+ -- the per-session mutable, epoch-fenced active-sandbox POINTER (M2 of the
3
+ -- bring-your-own-compute mega-PR; design-of-record .agent/implementation-dossier.md
4
+ -- §10.3 routing proxy + §10.7 metrics + §23 enrollment).
5
+ --
6
+ -- WHY these four tables land together:
7
+ -- * enrollments — a user's own machine, registered once (the agent's
8
+ -- ed25519 pubkey IS its identity; whole-machine consent +
9
+ -- display/screen-control capture; active|revoked).
10
+ -- * sandboxes — the first-class NAMED sandbox a session's active pointer
11
+ -- points AT (kind modal|selfhosted; a selfhosted sandbox
12
+ -- carries enrollment_id → the machine it lives on).
13
+ -- * sessions.active_sandbox_id / active_epoch — the mutable, epoch-fenced pointer
14
+ -- (the second epoch ABOVE lease_epoch) the routing proxy
15
+ -- re-reads PER TOOL CALL to make hot-swap seamless.
16
+ -- * machine_metrics_{latest,series} — last-sample upsert + ~1/min downsampled
17
+ -- series per enrollment (NOT Prometheus; §10.7).
18
+ --
19
+ -- INTEGER epoch (NOT bigint): the lease-epoch spike proved postgres-js returns int8
20
+ -- from a raw query as a JS STRING, which breaks a strict epoch-fence compare. The
21
+ -- active_epoch fence shares that discipline — int4 returns a JS number. Epochs never
22
+ -- approach 2^31, so the narrower type loses nothing (same rationale as
23
+ -- sandbox_leases.lease_epoch in 0017).
24
+ --
25
+ -- DDL is INERT until the M3+ provider/routing/enrollment code wires it (gated behind
26
+ -- sandboxSelfhostedEnabled). Forward-only + behavior-preserving: active_sandbox_id
27
+ -- defaults to NULL (a NULL pointer resolves to the session's own group sandbox — the
28
+ -- backward-compat default of §10.3), active_epoch defaults to 0; no backfill needed.
29
+ --
30
+ -- ROLLBACK (forward-only repo, but each statement is cleanly reversible): the down
31
+ -- order is the FK-reverse of the up order —
32
+ -- DROP TABLE machine_metrics_series;
33
+ -- DROP TABLE machine_metrics_latest;
34
+ -- ALTER TABLE sessions DROP COLUMN active_epoch, DROP COLUMN active_sandbox_id;
35
+ -- DROP TABLE sandboxes;
36
+ -- DROP TABLE enrollments;
37
+ -- (the migration up/down/up gate exercises exactly this.)
38
+
39
+ -- ============== enrollments (one row per registered machine) =================
40
+ -- The agent's ed25519 PUBLIC key is the machine's identity (the control-plane
41
+ -- subject the agent subscribes to maps to it). exposure is the loudly-consented
42
+ -- access mode ('whole-machine' today; the column is text+CHECK so a future
43
+ -- narrower mode is a CHECK widening, not a re-key). has_display/allow_screen_control
44
+ -- are the desktop/computer-use consent bits (default FALSE — consent is opt-in).
45
+ -- status is the lifecycle: 'active' until the user revokes (uninstall --purge /
46
+ -- dashboard revoke), then 'revoked' (revoked_at stamped). last_seen_at is the
47
+ -- heartbeat-driven liveness cursor surfaced in the Machines dashboard.
48
+ CREATE TABLE IF NOT EXISTS "enrollments" (
49
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
50
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE CASCADE,
51
+ "workspace_id" uuid NOT NULL REFERENCES "workspaces"("id") ON DELETE CASCADE,
52
+
53
+ -- The agent's ed25519 public key (the machine identity). Unique per workspace —
54
+ -- one machine enrolls once per workspace (a re-enroll of the same key is an
55
+ -- idempotent UPDATE, not a second row).
56
+ "pubkey" text NOT NULL,
57
+
58
+ -- The loudly-consented access mode. 'whole-machine' is the only mode today.
59
+ "exposure" text NOT NULL DEFAULT 'whole-machine'
60
+ CHECK ("exposure" IN ('whole-machine')),
61
+
62
+ -- Desktop / computer-use consent bits (default FALSE — opt-in per §3/§18).
63
+ "has_display" boolean NOT NULL DEFAULT false,
64
+ "allow_screen_control" boolean NOT NULL DEFAULT false,
65
+
66
+ -- Lifecycle. 'active' until revoked; the reaper / Machines list filter on it.
67
+ "status" text NOT NULL DEFAULT 'active'
68
+ CHECK ("status" IN ('active','revoked')),
69
+
70
+ -- The machine's OS/arch (linux|macos|windows + the cargo arch). Reported at
71
+ -- enroll; informs the asset/desktop-capability decisions.
72
+ "os" text NOT NULL DEFAULT 'linux'
73
+ CHECK ("os" IN ('linux','macos','windows')),
74
+ "arch" text NOT NULL DEFAULT 'x86_64',
75
+
76
+ -- Heartbeat liveness cursor (online/reconnecting/offline derive from this +
77
+ -- the §10.6 thresholds). NULL until the first connect.
78
+ "last_seen_at" timestamptz,
79
+
80
+ "created_at" timestamptz NOT NULL DEFAULT now(),
81
+ "revoked_at" timestamptz,
82
+ "updated_at" timestamptz NOT NULL DEFAULT now()
83
+ );
84
+
85
+ -- One enrollment per (workspace, pubkey): a re-enroll of the same machine is an
86
+ -- idempotent upsert, never a duplicate machine row.
87
+ CREATE UNIQUE INDEX IF NOT EXISTS "enrollments_workspace_pubkey_idx"
88
+ ON "enrollments" ("workspace_id", "pubkey");
89
+
90
+ -- List a workspace's ACTIVE machines for the Machines dashboard without scanning
91
+ -- revoked rows.
92
+ CREATE INDEX IF NOT EXISTS "enrollments_workspace_status_idx"
93
+ ON "enrollments" ("workspace_id", "status");
94
+
95
+ -- ============== sandboxes (the first-class named sandbox pointer target) ======
96
+ -- The row a session's active_sandbox_id points AT. kind discriminates the backend
97
+ -- the routing proxy resolves to: 'modal' (a cloud box) or 'selfhosted' (a user's
98
+ -- machine, carrying enrollment_id → the enrollment it lives on). A modal sandbox
99
+ -- has NULL enrollment_id; a selfhosted sandbox MUST carry one (enforced by the
100
+ -- partial CHECK below). enrollment_id is ON DELETE SET NULL so deleting an
101
+ -- enrollment never cascade-kills a sandbox row a session might still point at —
102
+ -- the routing layer surfaces agent_offline instead.
103
+ CREATE TABLE IF NOT EXISTS "sandboxes" (
104
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
105
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE CASCADE,
106
+ "workspace_id" uuid NOT NULL REFERENCES "workspaces"("id") ON DELETE CASCADE,
107
+
108
+ -- The backend this sandbox resolves to.
109
+ "kind" text NOT NULL CHECK ("kind" IN ('modal','selfhosted')),
110
+
111
+ -- A human-facing name (the Machines/sandbox-list label).
112
+ "name" text NOT NULL,
113
+
114
+ -- The enrollment a selfhosted sandbox lives on. NULL for a modal sandbox; SET
115
+ -- NULL (not CASCADE) on enrollment delete so a pointed-at sandbox row is never
116
+ -- swept out from under a session.
117
+ "enrollment_id" uuid REFERENCES "enrollments"("id") ON DELETE SET NULL,
118
+
119
+ "created_at" timestamptz NOT NULL DEFAULT now(),
120
+ "updated_at" timestamptz NOT NULL DEFAULT now(),
121
+
122
+ -- A selfhosted sandbox is meaningless without its machine; a modal sandbox has
123
+ -- no enrollment. Pin the invariant at the DB edge.
124
+ CONSTRAINT "sandboxes_selfhosted_enrollment_chk"
125
+ CHECK (("kind" = 'selfhosted' AND "enrollment_id" IS NOT NULL)
126
+ OR ("kind" <> 'selfhosted' AND "enrollment_id" IS NULL))
127
+ );
128
+
129
+ -- List a workspace's sandboxes (the sandboxes_list tool / Machines surface).
130
+ CREATE INDEX IF NOT EXISTS "sandboxes_workspace_created_idx"
131
+ ON "sandboxes" ("workspace_id", "created_at");
132
+
133
+ -- Enumerate the sandboxes living on one enrollment (a machine's sandbox; a
134
+ -- selfhosted enrollment is maxSandboxes:1, but the index is general).
135
+ CREATE INDEX IF NOT EXISTS "sandboxes_enrollment_idx"
136
+ ON "sandboxes" ("enrollment_id")
137
+ WHERE "enrollment_id" IS NOT NULL;
138
+
139
+ -- ============== sessions.active_sandbox_id + active_epoch (the pointer) =======
140
+ -- The mutable, epoch-fenced pointer the routing proxy re-reads PER TOOL CALL.
141
+ -- active_sandbox_id NULL == "use the session's own group sandbox" (the §10.3
142
+ -- backward-compat default — no backfill flips existing rows). ON DELETE SET NULL
143
+ -- so deleting a sandbox a session points at degrades to the group default, never
144
+ -- a dangling FK. active_epoch is the SECOND epoch ABOVE lease_epoch, bumped on
145
+ -- every swap; an in-flight op fenced by a stale active_epoch retries against the
146
+ -- new active sandbox. integer (NOT bigint) — see header.
147
+ ALTER TABLE "sessions"
148
+ ADD COLUMN IF NOT EXISTS "active_sandbox_id" uuid
149
+ REFERENCES "sandboxes"("id") ON DELETE SET NULL,
150
+ ADD COLUMN IF NOT EXISTS "active_epoch" integer NOT NULL DEFAULT 0;
151
+
152
+ -- ============== machine_metrics_latest (one row per enrollment) ===============
153
+ -- Last-sample upsert: ONE row per enrollment, overwritten on every sample. The
154
+ -- PRIMARY KEY on enrollment_id is the upsert conflict target (ON CONFLICT
155
+ -- (enrollment_id) DO UPDATE). ON DELETE CASCADE — metrics die with the machine.
156
+ -- The sampled signals (§10.7): CPU%, load1/5/15, RAM/disk used+total, optional
157
+ -- GPU util/mem, and a contention signal (run-queue length / pressure). Nullable
158
+ -- where a platform/sample may not provide it (no GPU, headless).
159
+ CREATE TABLE IF NOT EXISTS "machine_metrics_latest" (
160
+ "enrollment_id" uuid PRIMARY KEY REFERENCES "enrollments"("id") ON DELETE CASCADE,
161
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE CASCADE,
162
+ "workspace_id" uuid NOT NULL REFERENCES "workspaces"("id") ON DELETE CASCADE,
163
+
164
+ "cpu_percent" numeric,
165
+ "load1" numeric,
166
+ "load5" numeric,
167
+ "load15" numeric,
168
+ "mem_used_bytes" bigint,
169
+ "mem_total_bytes" bigint,
170
+ "disk_used_bytes" bigint,
171
+ "disk_total_bytes" bigint,
172
+ "gpu_util_percent" numeric,
173
+ "gpu_mem_used_bytes" bigint,
174
+ "gpu_mem_total_bytes" bigint,
175
+ "contention" numeric,
176
+
177
+ -- When the agent took the sample (the agent's clock) vs when we stored it.
178
+ "sampled_at" timestamptz NOT NULL,
179
+ "updated_at" timestamptz NOT NULL DEFAULT now()
180
+ );
181
+
182
+ CREATE INDEX IF NOT EXISTS "machine_metrics_latest_workspace_idx"
183
+ ON "machine_metrics_latest" ("workspace_id");
184
+
185
+ -- ============== machine_metrics_series (downsampled ~1/min, retained N days) ===
186
+ -- Append-only downsampled history (one row ~per minute per enrollment). Retention
187
+ -- (delete rows older than N days) is a later concern; the table shape lands here.
188
+ -- Same signal columns as _latest. The (enrollment_id, sampled_at) index serves the
189
+ -- dashboard time-range read AND the retention sweep.
190
+ CREATE TABLE IF NOT EXISTS "machine_metrics_series" (
191
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
192
+ "enrollment_id" uuid NOT NULL REFERENCES "enrollments"("id") ON DELETE CASCADE,
193
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE CASCADE,
194
+ "workspace_id" uuid NOT NULL REFERENCES "workspaces"("id") ON DELETE CASCADE,
195
+
196
+ "cpu_percent" numeric,
197
+ "load1" numeric,
198
+ "load5" numeric,
199
+ "load15" numeric,
200
+ "mem_used_bytes" bigint,
201
+ "mem_total_bytes" bigint,
202
+ "disk_used_bytes" bigint,
203
+ "disk_total_bytes" bigint,
204
+ "gpu_util_percent" numeric,
205
+ "gpu_mem_used_bytes" bigint,
206
+ "gpu_mem_total_bytes" bigint,
207
+ "contention" numeric,
208
+
209
+ "sampled_at" timestamptz NOT NULL,
210
+ "created_at" timestamptz NOT NULL DEFAULT now()
211
+ );
212
+
213
+ -- Dashboard time-range read (newest-first per machine) + the retention sweep.
214
+ CREATE INDEX IF NOT EXISTS "machine_metrics_series_enrollment_sampled_idx"
215
+ ON "machine_metrics_series" ("enrollment_id", "sampled_at");
216
+
217
+ -- The retention sweep scans by age across all machines.
218
+ CREATE INDEX IF NOT EXISTS "machine_metrics_series_sampled_idx"
219
+ ON "machine_metrics_series" ("sampled_at");
220
+
221
+ -- ============== RLS + grants (verbatim 0017/0021 boilerplate) ================
222
+ -- Every new table is workspace-scoped behind the SAME workspace_rls_visible policy
223
+ -- the lease/pty tables use, so a scoped opengeni_app connection only ever sees its
224
+ -- own workspace's machines/sandboxes/metrics.
225
+ ALTER TABLE "enrollments" ENABLE ROW LEVEL SECURITY;
226
+ ALTER TABLE "enrollments" FORCE ROW LEVEL SECURITY;
227
+ ALTER TABLE "sandboxes" ENABLE ROW LEVEL SECURITY;
228
+ ALTER TABLE "sandboxes" FORCE ROW LEVEL SECURITY;
229
+ ALTER TABLE "machine_metrics_latest" ENABLE ROW LEVEL SECURITY;
230
+ ALTER TABLE "machine_metrics_latest" FORCE ROW LEVEL SECURITY;
231
+ ALTER TABLE "machine_metrics_series" ENABLE ROW LEVEL SECURITY;
232
+ ALTER TABLE "machine_metrics_series" FORCE ROW LEVEL SECURITY;
233
+
234
+ DO $$
235
+ DECLARE
236
+ t text;
237
+ BEGIN
238
+ FOREACH t IN ARRAY ARRAY['enrollments','sandboxes','machine_metrics_latest','machine_metrics_series']
239
+ LOOP
240
+ IF EXISTS (
241
+ SELECT 1 FROM pg_policies
242
+ WHERE schemaname = current_schema() AND tablename = t AND policyname = 'workspace_isolation'
243
+ ) THEN
244
+ EXECUTE format('DROP POLICY workspace_isolation ON %I', t);
245
+ END IF;
246
+ EXECUTE format(
247
+ 'CREATE POLICY workspace_isolation ON %I '
248
+ 'USING (opengeni_private.workspace_rls_visible(account_id, workspace_id)) '
249
+ 'WITH CHECK (opengeni_private.workspace_rls_visible(account_id, workspace_id))',
250
+ t);
251
+ END LOOP;
252
+ END $$;
253
+
254
+ DO $$
255
+ BEGIN
256
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
257
+ GRANT SELECT, INSERT, UPDATE, DELETE ON "enrollments" TO opengeni_app;
258
+ GRANT SELECT, INSERT, UPDATE, DELETE ON "sandboxes" TO opengeni_app;
259
+ GRANT SELECT, INSERT, UPDATE, DELETE ON "machine_metrics_latest" TO opengeni_app;
260
+ GRANT SELECT, INSERT, UPDATE, DELETE ON "machine_metrics_series" TO opengeni_app;
261
+ END IF;
262
+ END $$;
@@ -0,0 +1,142 @@
1
+ -- The OAuth 2.0 device-authorization (RFC 8628) PENDING request store (M5 of the
2
+ -- bring-your-own-compute mega-PR; design-of-record .agent/implementation-dossier.md
3
+ -- §10.2 enrollment device-flow + §18 LOUD consent). One short-TTL, SINGLE-USE row
4
+ -- per in-flight enrollment, keyed by an opaque device_code (the agent polls with) +
5
+ -- a short user_code (the user types at the approve page).
6
+ --
7
+ -- WHY a table (not in-memory / a signed stateless code): the agent polling and the
8
+ -- user approving can hit DIFFERENT api replicas, and the consent record (WHO
9
+ -- approved WHEN to WHAT) must be durable + auditable. An in-memory map would break
10
+ -- across replicas and lose the consent trail. Postgres is the consistent,
11
+ -- multi-replica-safe store the rest of the control plane already uses (no KV/Redis
12
+ -- in the API). Mirrors the M2 0024 conventions verbatim (RLS + grants boilerplate).
13
+ --
14
+ -- STATE MACHINE: pending → approved | denied. A pending row past expires_at is
15
+ -- EXPIRED on poll (no separate state — the expiry is read at poll time). Once the
16
+ -- agent polls an approved row's credentials, the row flips to 'consumed' (so the
17
+ -- credentials are single-use). The DURABLE identity the approve produced is the
18
+ -- `enrollments` row (+ a `sandboxes` row, acceptance #2); this request row is
19
+ -- transient and a retention sweep prunes terminal rows.
20
+ --
21
+ -- ROLLBACK (forward-only repo, but cleanly reversible):
22
+ -- DROP TABLE device_enrollment_requests;
23
+ -- (the migration up/down/up gate exercises exactly this.)
24
+
25
+ -- The lifecycle domain (text + CHECK so a future state is a CHECK widening, not a
26
+ -- re-key — same discipline as enrollments.status / sandboxes.kind in 0024).
27
+ CREATE TABLE IF NOT EXISTS "device_enrollment_requests" (
28
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
29
+ -- The opaque code the agent polls with (unguessable, single-use). Globally unique.
30
+ "device_code" text NOT NULL,
31
+ -- The short human-typed code (e.g. 'WDJB-MJHT'). Unique among LIVE (pending) rows.
32
+ "user_code" text NOT NULL,
33
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE CASCADE,
34
+ "workspace_id" uuid NOT NULL REFERENCES "workspaces"("id") ON DELETE CASCADE,
35
+ -- The agent's ed25519 public key (the machine identity the enrollment binds to).
36
+ "pubkey" text NOT NULL,
37
+ "os" text NOT NULL DEFAULT 'linux',
38
+ "arch" text NOT NULL DEFAULT 'x86_64',
39
+ "machine_name" text,
40
+ -- The exposure the agent REQUESTED (whole-machine in v1; loudly consented at approve).
41
+ "requested_exposure" text NOT NULL DEFAULT 'whole-machine',
42
+ -- The agent CAN offer a display (a real screen / Xvfb is available).
43
+ "can_offer_display" boolean NOT NULL DEFAULT false,
44
+ -- The agent REQUESTS screen control (computer-use); the user's allow_screen_control
45
+ -- at approve is the AUTHORITATIVE consent.
46
+ "requests_screen_control" boolean NOT NULL DEFAULT false,
47
+ "status" text NOT NULL DEFAULT 'pending',
48
+ -- ── LOUD CONSENT capture (who/when/what), stamped at approve ────────────────
49
+ "approved_by_subject_id" text,
50
+ "approved_by_subject_label" text,
51
+ -- The user's screen-control consent decision (whole-machine is mandatory at approve).
52
+ "allow_screen_control" boolean NOT NULL DEFAULT false,
53
+ "approved_at" timestamptz,
54
+ -- The enrollment + sandbox the approve produced (acceptance #2). Null until approved.
55
+ -- ON DELETE SET NULL so deleting an enrollment never cascade-kills this audit row.
56
+ "enrollment_id" uuid REFERENCES "enrollments"("id") ON DELETE SET NULL,
57
+ "sandbox_id" uuid REFERENCES "sandboxes"("id") ON DELETE SET NULL,
58
+ "expires_at" timestamptz NOT NULL,
59
+ "created_at" timestamptz NOT NULL DEFAULT now(),
60
+ "updated_at" timestamptz NOT NULL DEFAULT now(),
61
+ CONSTRAINT "device_enrollment_requests_status_chk"
62
+ CHECK ("status" IN ('pending', 'approved', 'denied', 'consumed')),
63
+ CONSTRAINT "device_enrollment_requests_exposure_chk"
64
+ CHECK ("requested_exposure" IN ('whole-machine')),
65
+ CONSTRAINT "device_enrollment_requests_os_chk"
66
+ CHECK ("os" IN ('linux', 'macos', 'windows'))
67
+ );
68
+
69
+ -- The device_code is the agent's poll key — globally unique + indexed.
70
+ CREATE UNIQUE INDEX IF NOT EXISTS "device_enrollment_requests_device_code_idx"
71
+ ON "device_enrollment_requests" ("device_code");
72
+
73
+ -- The user_code must be unique among LIVE (pending) rows so the approve lookup is
74
+ -- unambiguous; a terminal row's code may be recycled (partial unique index).
75
+ CREATE UNIQUE INDEX IF NOT EXISTS "device_enrollment_requests_user_code_pending_idx"
76
+ ON "device_enrollment_requests" ("user_code")
77
+ WHERE "status" = 'pending';
78
+
79
+ CREATE INDEX IF NOT EXISTS "device_enrollment_requests_workspace_created_idx"
80
+ ON "device_enrollment_requests" ("workspace_id", "created_at");
81
+
82
+ CREATE INDEX IF NOT EXISTS "device_enrollment_requests_expires_idx"
83
+ ON "device_enrollment_requests" ("expires_at");
84
+
85
+ -- ============== RLS + grants (verbatim 0017/0021/0024 boilerplate) ===========
86
+ -- Workspace-scoped behind the SAME workspace_rls_visible policy the lease/pty/
87
+ -- enrollment tables use, so a scoped opengeni_app connection only ever sees its own
88
+ -- workspace's pending device-auth requests.
89
+ ALTER TABLE "device_enrollment_requests" ENABLE ROW LEVEL SECURITY;
90
+ ALTER TABLE "device_enrollment_requests" FORCE ROW LEVEL SECURITY;
91
+
92
+ DO $$
93
+ BEGIN
94
+ IF EXISTS (
95
+ SELECT 1 FROM pg_policies
96
+ WHERE schemaname = current_schema() AND tablename = 'device_enrollment_requests' AND policyname = 'workspace_isolation'
97
+ ) THEN
98
+ DROP POLICY workspace_isolation ON "device_enrollment_requests";
99
+ END IF;
100
+ CREATE POLICY workspace_isolation ON "device_enrollment_requests"
101
+ USING (opengeni_private.workspace_rls_visible(account_id, workspace_id))
102
+ WITH CHECK (opengeni_private.workspace_rls_visible(account_id, workspace_id));
103
+ END $$;
104
+
105
+ DO $$
106
+ BEGIN
107
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
108
+ GRANT SELECT, INSERT, UPDATE, DELETE ON "device_enrollment_requests" TO opengeni_app;
109
+ END IF;
110
+ END $$;
111
+
112
+ -- ============== device_code → (account_id, workspace_id) resolver ============
113
+ -- The agent's POST /poll presents ONLY the opaque device_code — it has NO
114
+ -- workspace context yet (it isn't enrolled). FORCE RLS would block a scoped
115
+ -- connection from reading the row to discover which workspace it belongs to. So,
116
+ -- exactly like the global reaper's cross-workspace sweep (0017
117
+ -- opengeni_private.reap_sandbox_leases), the lookup is a SECURITY DEFINER read fn
118
+ -- that returns ONLY the (account_id, workspace_id) for an UNEXPIRED-or-recent row.
119
+ -- The DAO then re-reads the FULL row under the resolved workspace's RLS scope, so
120
+ -- the device_code is the capability (unguessable + unique) and no broad table read
121
+ -- ever escapes RLS. Returns no rows for an unknown code.
122
+ CREATE OR REPLACE FUNCTION opengeni_private.resolve_device_enrollment_request(
123
+ p_device_code text
124
+ )
125
+ RETURNS TABLE (account_id uuid, workspace_id uuid)
126
+ LANGUAGE sql
127
+ SECURITY DEFINER
128
+ -- EMBED-SAFE (see 0017): no pinned 'public' search_path -- inherit the caller's path so
129
+ -- device_enrollment_requests resolves in public (standalone) OR the dedicated schema (embed).
130
+ AS $$
131
+ SELECT d.account_id, d.workspace_id
132
+ FROM device_enrollment_requests d
133
+ WHERE d.device_code = p_device_code
134
+ LIMIT 1
135
+ $$;
136
+
137
+ DO $$
138
+ BEGIN
139
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
140
+ GRANT EXECUTE ON FUNCTION opengeni_private.resolve_device_enrollment_request(text) TO opengeni_app;
141
+ END IF;
142
+ END $$;
@@ -0,0 +1,47 @@
1
+ -- The user_code → (account_id, workspace_id) resolver for the click-Grant approve
2
+ -- page lookup (self-hosted enrollment UX, design 11 §B.1). Companion to the
3
+ -- device_code resolver in 0025 (resolve_device_enrollment_request).
4
+ --
5
+ -- WHY a SECURITY DEFINER resolver: POST /v1/enrollments/device/lookup is the
6
+ -- approve page reading machine details for a user_code WITHOUT a workspace in the
7
+ -- path — the user_code is globally unique among LIVE (status='pending') rows (the
8
+ -- 0025 partial unique index device_enrollment_requests_user_code_pending_idx). A
9
+ -- scoped opengeni_app connection is under FORCE RLS and cannot read a row to learn
10
+ -- which workspace it belongs to, so — exactly like 0025's device_code resolver —
11
+ -- this returns ONLY the (account_id, workspace_id) for the PENDING row matching the
12
+ -- code. The DAO then re-reads the FULL row under the resolved workspace's RLS scope
13
+ -- and the ROUTE re-checks the caller holds an enrollments:read grant in THAT
14
+ -- workspace, so the user_code is the capability and no broad read escapes RLS.
15
+ --
16
+ -- PENDING-ONLY: the partial unique index guarantees AT MOST ONE pending row per
17
+ -- user_code, so the resolver is unambiguous; a terminal (approved/denied/consumed)
18
+ -- row's recycled code is intentionally invisible here (lookup is for the live
19
+ -- approve decision only).
20
+ --
21
+ -- ROLLBACK (forward-only repo, but cleanly reversible):
22
+ -- DROP FUNCTION IF EXISTS opengeni_private.resolve_pending_device_enrollment_by_user_code(text);
23
+
24
+ CREATE OR REPLACE FUNCTION opengeni_private.resolve_pending_device_enrollment_by_user_code(
25
+ p_user_code text
26
+ )
27
+ RETURNS TABLE (account_id uuid, workspace_id uuid)
28
+ LANGUAGE sql
29
+ SECURITY DEFINER
30
+ -- EMBED-SAFE (see 0017/0025): no pinned 'public' search_path -- inherit the caller's path so
31
+ -- device_enrollment_requests resolves in public (standalone) OR the dedicated schema (embed).
32
+ -- A pinned `public` path also fails CREATE-time body validation (check_function_bodies) under
33
+ -- the dedicated-schema migrate, since device_enrollment_requests lives in <schema>, not public.
34
+ AS $$
35
+ SELECT d.account_id, d.workspace_id
36
+ FROM device_enrollment_requests d
37
+ WHERE d.user_code = p_user_code
38
+ AND d.status = 'pending'
39
+ LIMIT 1
40
+ $$;
41
+
42
+ DO $$
43
+ BEGIN
44
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
45
+ GRANT EXECUTE ON FUNCTION opengeni_private.resolve_pending_device_enrollment_by_user_code(text) TO opengeni_app;
46
+ END IF;
47
+ END $$;
@@ -0,0 +1,24 @@
1
+ -- Per-session working directory (Stage A: backend slice).
2
+ --
3
+ -- sessions.working_dir is the path/cwd BASE the session's (selfhosted) box
4
+ -- operates under — its agent exec, terminal pty, and structural file dock. The
5
+ -- value is a launch-workspace_root-relative subdir (resolved under workspace_root
6
+ -- by the agent's resolve_cwd) or an absolute machine path.
7
+ --
8
+ -- NULL (the default) ⇒ today's behavior EXACTLY: the agent substitutes its launch
9
+ -- workspace_root for an empty cwd, so an unset working_dir is a byte-identical
10
+ -- no-op (every existing + new row is NULL, no backfill). It is surfaced alongside
11
+ -- the active-sandbox pointer (readActiveSandbox) and written through the
12
+ -- epoch-fenced setActiveSandbox CAS, never the row INSERT — so it is seeded
13
+ -- create-time when a machine target is named, per-session.
14
+ ALTER TABLE "sessions"
15
+ ADD COLUMN IF NOT EXISTS "working_dir" text;
16
+
17
+ -- Re-grant on the new column (idempotent; mirrors the boilerplate in earlier
18
+ -- migrations so a fresh opengeni_app role can read/write the added column).
19
+ DO $$
20
+ BEGIN
21
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
22
+ GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO opengeni_app;
23
+ END IF;
24
+ END $$;