@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,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 $$;
|