@opengeni/db 3.7.4 → 4.0.0-canary.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 (113) hide show
  1. package/dist/automations.d.ts +4 -0
  2. package/dist/canonical-human-identities.js +3 -3
  3. package/dist/capability-integrations.d.ts +2 -0
  4. package/dist/{chunk-6DPJA36C.js → chunk-2XEZK4LF.js} +7 -1
  5. package/dist/chunk-2XEZK4LF.js.map +1 -0
  6. package/dist/{chunk-4NAR7FBA.js → chunk-3RI33YQN.js} +67 -1
  7. package/dist/chunk-3RI33YQN.js.map +1 -0
  8. package/dist/{chunk-SZAWDXQC.js → chunk-CJROYHSC.js} +3 -3
  9. package/dist/{chunk-DZCYW2WT.js → chunk-MX6V5F65.js} +2 -2
  10. package/dist/{chunk-S3TDDCS2.js → chunk-N5VXJ4I2.js} +151 -78
  11. package/dist/chunk-N5VXJ4I2.js.map +1 -0
  12. package/dist/{chunk-U4WPPEBH.js → chunk-NLFHMPUB.js} +2 -2
  13. package/dist/{chunk-5NNCGFQY.js → chunk-P6NSEG2S.js} +2 -2
  14. package/dist/{chunk-O56DM5GZ.js → chunk-QM6YOWHZ.js} +624 -57
  15. package/dist/chunk-QM6YOWHZ.js.map +1 -0
  16. package/dist/{chunk-US4IS6IV.js → chunk-RZNOSEMC.js} +2 -2
  17. package/dist/{chunk-F6PBS6WE.js → chunk-UFAMOUWL.js} +2 -2
  18. package/dist/codemode-operations.d.ts +5 -0
  19. package/dist/codex-token-resolver.d.ts +6 -2
  20. package/dist/connection-token-resolver.d.ts +8 -0
  21. package/dist/editable-artifact-durable-export.js +2 -2
  22. package/dist/editable-artifacts.js +3 -3
  23. package/dist/generated-images.d.ts +15 -0
  24. package/dist/index.d.ts +393 -91
  25. package/dist/index.js +9992 -6069
  26. package/dist/index.js.map +1 -1
  27. package/dist/insights.d.ts +9 -0
  28. package/dist/managed-auth-session-sets.js +3 -3
  29. package/dist/managed-user-setup.d.ts +3 -0
  30. package/dist/mcp-oauth.d.ts +80 -0
  31. package/dist/migrate.d.ts +1 -0
  32. package/dist/migrate.js +3 -1
  33. package/dist/model-catalog.d.ts +153 -0
  34. package/dist/model-context-snapshots.d.ts +20 -0
  35. package/dist/new-session-drafts.d.ts +12 -0
  36. package/dist/organization-membership-lifecycle.d.ts +8 -1
  37. package/dist/organization-model-providers.d.ts +101 -0
  38. package/dist/pack-components.d.ts +1 -0
  39. package/dist/pr-review.d.ts +9 -0
  40. package/dist/provision-roles.js +1 -1
  41. package/dist/runtime-posture.d.ts +8 -8
  42. package/dist/schema.d.ts +7708 -5050
  43. package/dist/schema.js +25 -1
  44. package/dist/session-background-commands.d.ts +18 -6
  45. package/dist/session-background-commands.js +15 -9
  46. package/dist/session-control.d.ts +37 -2
  47. package/dist/session-goal-pacing.d.ts +3 -6
  48. package/dist/session-queue-commands.d.ts +17 -0
  49. package/dist/session-tenancy.js +3 -3
  50. package/dist/tool-gateway-approvals.d.ts +31 -0
  51. package/dist/video-generation.js +3 -3
  52. package/dist/workspace-artifacts.d.ts +17 -3
  53. package/dist/xai-subscription.d.ts +8 -0
  54. package/drizzle/0387_bound_host_export_session_payloads.sql +165 -0
  55. package/drizzle/0388_sandbox_provider_deadline_interactions.sql +491 -0
  56. package/drizzle/0389_model_catalog_and_gateway_custom_models.sql +241 -0
  57. package/drizzle/0390_organization_model_provider_connections.sql +197 -0
  58. package/drizzle/0391_sandbox_provider_deadline_interaction_followup.sql +295 -0
  59. package/drizzle/0392_context_compaction_pending_observability.sql +198 -0
  60. package/drizzle/0393_workspace_memory_and_learning_defaults.sql +102 -0
  61. package/drizzle/0394_session_selected_skill_activation.sql +20 -0
  62. package/drizzle/0395_scheduled_task_unclaimed_occurrence_invalidation.sql +154 -0
  63. package/drizzle/0396_model_call_equivalent_credit_cost.sql +28 -0
  64. package/drizzle/0397_sandbox_deadline_rotation_preemption.sql +113 -0
  65. package/drizzle/0398_organization_workspace_management_entry.sql +206 -0
  66. package/drizzle/0399_additional_managed_organization_creation.sql +691 -0
  67. package/drizzle/0400_session_attempt_model_context_snapshots.sql +65 -0
  68. package/drizzle/0401_organization_user_setup_token_transport.sql +178 -0
  69. package/drizzle/0402_session_input_wait_and_background_command_results.sql +1023 -0
  70. package/drizzle/0403_codex_unconditional_credential_leasing.sql +181 -0
  71. package/drizzle/0404_mcp_oauth_authorization_server.sql +471 -0
  72. package/drizzle/0405_tool_gateway_approval_capabilities.sql +184 -0
  73. package/drizzle/0406_workspace_html_site_sources.sql +71 -0
  74. package/package.json +6 -6
  75. package/src/automations.ts +9 -1
  76. package/src/capability-integrations.ts +8 -0
  77. package/src/codemode-operations.ts +31 -21
  78. package/src/codex-token-resolver.ts +16 -6
  79. package/src/connection-token-resolver.ts +52 -3
  80. package/src/generated-images.ts +70 -6
  81. package/src/index.ts +3894 -994
  82. package/src/insights-model-bundle.ts +54 -5
  83. package/src/insights.ts +53 -7
  84. package/src/managed-user-setup.ts +14 -3
  85. package/src/mcp-oauth.ts +488 -0
  86. package/src/migrate.ts +9 -1
  87. package/src/model-catalog.ts +682 -0
  88. package/src/model-context-snapshots.ts +151 -0
  89. package/src/new-session-drafts.ts +66 -21
  90. package/src/organization-membership-lifecycle.ts +47 -5
  91. package/src/organization-model-providers.ts +614 -0
  92. package/src/pack-components.ts +15 -2
  93. package/src/pr-review.ts +19 -0
  94. package/src/provision-roles.ts +4 -0
  95. package/src/runtime-posture.ts +64 -0
  96. package/src/schema.ts +666 -61
  97. package/src/session-background-commands.ts +180 -78
  98. package/src/session-control.ts +181 -12
  99. package/src/session-goal-pacing.ts +4 -8
  100. package/src/session-queue-commands.ts +157 -82
  101. package/src/tool-gateway-approvals.ts +168 -0
  102. package/src/workspace-artifacts.ts +633 -178
  103. package/src/xai-subscription.ts +66 -14
  104. package/dist/chunk-4NAR7FBA.js.map +0 -1
  105. package/dist/chunk-6DPJA36C.js.map +0 -1
  106. package/dist/chunk-O56DM5GZ.js.map +0 -1
  107. package/dist/chunk-S3TDDCS2.js.map +0 -1
  108. /package/dist/{chunk-SZAWDXQC.js.map → chunk-CJROYHSC.js.map} +0 -0
  109. /package/dist/{chunk-DZCYW2WT.js.map → chunk-MX6V5F65.js.map} +0 -0
  110. /package/dist/{chunk-U4WPPEBH.js.map → chunk-NLFHMPUB.js.map} +0 -0
  111. /package/dist/{chunk-5NNCGFQY.js.map → chunk-P6NSEG2S.js.map} +0 -0
  112. /package/dist/{chunk-US4IS6IV.js.map → chunk-RZNOSEMC.js.map} +0 -0
  113. /package/dist/{chunk-F6PBS6WE.js.map → chunk-UFAMOUWL.js.map} +0 -0
@@ -0,0 +1,181 @@
1
+ -- deployment-mode: maintenance
2
+ -- Migration 0403 retires the temporary allocator cutover bits. Every Codex
3
+ -- turn now uses the durable credential lease protocol; rotation_enabled remains
4
+ -- the user-owned policy deciding whether a new lease may leave the active
5
+ -- account. Stop every pre-0403 API, control-worker, and turn-worker before this
6
+ -- migration, then start only the 0403-aware binary.
7
+
8
+ SET LOCAL lock_timeout = '5s';
9
+ SET LOCAL statement_timeout = '5min';
10
+
11
+ DO $codex_unconditional_leasing_runtime_drain_before_lock$
12
+ DECLARE
13
+ configured_roles_text text := nullif(
14
+ current_setting('opengeni.migration_application_roles', true), ''
15
+ );
16
+ configured_roles jsonb;
17
+ BEGIN
18
+ IF configured_roles_text IS NULL THEN
19
+ RAISE EXCEPTION
20
+ '0403 unconditional Codex leasing requires an explicit application database role list'
21
+ USING ERRCODE = '55000';
22
+ END IF;
23
+ BEGIN
24
+ configured_roles := configured_roles_text::jsonb;
25
+ EXCEPTION WHEN OTHERS THEN
26
+ RAISE EXCEPTION
27
+ '0403 unconditional Codex leasing received a malformed application database role list'
28
+ USING ERRCODE = '55000';
29
+ END;
30
+ IF jsonb_typeof(configured_roles) <> 'array'
31
+ OR jsonb_array_length(configured_roles) NOT BETWEEN 1 AND 16
32
+ OR EXISTS (
33
+ SELECT 1 FROM jsonb_array_elements(configured_roles) AS roles(value)
34
+ WHERE jsonb_typeof(value) <> 'string'
35
+ OR btrim(value #>> '{}') = ''
36
+ OR value #>> '{}' <> btrim(value #>> '{}')
37
+ OR octet_length(value #>> '{}') > 63
38
+ )
39
+ OR (
40
+ SELECT count(*) FROM jsonb_array_elements_text(configured_roles)
41
+ ) <> (
42
+ SELECT count(DISTINCT value)
43
+ FROM jsonb_array_elements_text(configured_roles) AS roles(value)
44
+ )
45
+ THEN
46
+ RAISE EXCEPTION
47
+ '0403 unconditional Codex leasing received an invalid application database role list'
48
+ USING ERRCODE = '55000';
49
+ END IF;
50
+ IF EXISTS (
51
+ SELECT 1 FROM pg_stat_activity activity
52
+ JOIN jsonb_array_elements_text(configured_roles) roles(role_name)
53
+ ON roles.role_name = activity.usename
54
+ WHERE activity.datname = current_database()
55
+ AND activity.pid <> pg_backend_pid()
56
+ )
57
+ THEN
58
+ RAISE EXCEPTION
59
+ '0403 unconditional Codex leasing requires all configured OpenGeni application database sessions to be stopped'
60
+ USING ERRCODE = '55000';
61
+ END IF;
62
+ END
63
+ $codex_unconditional_leasing_runtime_drain_before_lock$;
64
+
65
+ LOCK TABLE organization_codex_rotation_settings IN ACCESS EXCLUSIVE MODE;
66
+ LOCK TABLE codex_rotation_settings IN ACCESS EXCLUSIVE MODE;
67
+ LOCK TABLE session_goals IN ACCESS EXCLUSIVE MODE;
68
+ LOCK TABLE codex_capacity_waiters IN ACCESS EXCLUSIVE MODE;
69
+
70
+ DO $codex_unconditional_leasing_runtime_drain_after_lock$
71
+ DECLARE
72
+ configured_roles jsonb := current_setting(
73
+ 'opengeni.migration_application_roles', false
74
+ )::jsonb;
75
+ BEGIN
76
+ IF EXISTS (
77
+ SELECT 1 FROM pg_stat_activity activity
78
+ JOIN jsonb_array_elements_text(configured_roles) roles(role_name)
79
+ ON roles.role_name = activity.usename
80
+ WHERE activity.datname = current_database()
81
+ AND activity.pid <> pg_backend_pid()
82
+ )
83
+ THEN
84
+ RAISE EXCEPTION
85
+ '0403 unconditional Codex leasing requires all configured OpenGeni application database sessions to be stopped'
86
+ USING ERRCODE = '55000';
87
+ END IF;
88
+ END
89
+ $codex_unconditional_leasing_runtime_drain_after_lock$;
90
+
91
+ -- Preserve an active goal without allowing an unchanged bounded Codex
92
+ -- failover failure to synthesize a fresh turn and reset its per-turn budget.
93
+ ALTER TABLE session_goals
94
+ ADD COLUMN IF NOT EXISTS continuation_suppressed_turn_id uuid;
95
+
96
+ ALTER TABLE organization_codex_rotation_settings
97
+ DROP COLUMN IF EXISTS lease_rotation_enabled;
98
+
99
+ ALTER TABLE codex_rotation_settings
100
+ DROP COLUMN IF EXISTS lease_rotation_enabled;
101
+
102
+ -- Policy/status waits are woken by allocator, connection, source, and pin
103
+ -- mutations. They must not fan out provider quota reads on every waiter timer.
104
+ ALTER TABLE codex_capacity_waiters
105
+ DROP CONSTRAINT IF EXISTS codex_capacity_waiters_reset_kind_check;
106
+ ALTER TABLE codex_capacity_waiters
107
+ ADD CONSTRAINT codex_capacity_waiters_reset_kind_check
108
+ CHECK (reset_kind IN ('authoritative', 'bounded_refresh', 'mutation_only'));
109
+
110
+ -- Migration 0381 installed these organization lease guards with `now()`,
111
+ -- whose value is fixed at transaction start. Reinstall the same guards with
112
+ -- execution-time clock semantics before the durable lease protocol becomes
113
+ -- authoritative, so a long maintenance-adjacent transaction cannot treat an
114
+ -- already-expired lease as live.
115
+ DO $codex_execution_time_lease_guards$
116
+ DECLARE data_schema text := current_schema();
117
+ BEGIN
118
+ EXECUTE format($ddl$
119
+ CREATE OR REPLACE FUNCTION opengeni_private.codex_organization_live_lease_count(
120
+ p_account_id uuid,
121
+ p_credential_id uuid,
122
+ p_exclude_turn_id uuid
123
+ ) RETURNS integer
124
+ LANGUAGE plpgsql
125
+ SECURITY DEFINER
126
+ SET search_path = pg_catalog, %1$I
127
+ AS $function$
128
+ DECLARE live_count integer;
129
+ BEGIN
130
+ IF p_account_id IS NULL
131
+ OR p_credential_id IS NULL
132
+ OR p_account_id IS DISTINCT FROM opengeni_private.current_account_id()
133
+ OR opengeni_private.current_workspace_id() IS NULL
134
+ OR NOT EXISTS (
135
+ SELECT 1 FROM codex_subscription_credentials credential
136
+ WHERE credential.account_id = p_account_id
137
+ AND credential.id = p_credential_id
138
+ AND credential.organization_id = p_account_id
139
+ AND credential.authority_scope = 'organization'
140
+ )
141
+ OR NOT opengeni_private.codex_credential_serves_workspace(
142
+ p_account_id,
143
+ opengeni_private.current_workspace_id(),
144
+ p_credential_id
145
+ )
146
+ THEN
147
+ RAISE EXCEPTION 'organization Codex lease-count authority required'
148
+ USING ERRCODE = '42501';
149
+ END IF;
150
+ SELECT count(*)::integer INTO live_count
151
+ FROM codex_credential_leases lease
152
+ WHERE lease.account_id = p_account_id
153
+ AND lease.credential_id = p_credential_id
154
+ AND lease.leased_until > clock_timestamp()
155
+ AND (p_exclude_turn_id IS NULL OR lease.turn_id <> p_exclude_turn_id);
156
+ RETURN live_count;
157
+ END
158
+ $function$;
159
+
160
+ CREATE OR REPLACE FUNCTION opengeni_private.prevent_organization_codex_disconnect_with_live_leases()
161
+ RETURNS trigger
162
+ LANGUAGE plpgsql
163
+ SECURITY DEFINER
164
+ SET search_path = pg_catalog, %1$I
165
+ AS $function$
166
+ BEGIN
167
+ IF EXISTS (
168
+ SELECT 1 FROM codex_credential_leases lease
169
+ WHERE lease.account_id = OLD.account_id
170
+ AND lease.credential_id = OLD.id
171
+ AND lease.leased_until > clock_timestamp()
172
+ ) THEN
173
+ RAISE EXCEPTION 'Codex subscription cannot disconnect while active turns are using it'
174
+ USING ERRCODE = '55006';
175
+ END IF;
176
+ RETURN OLD;
177
+ END
178
+ $function$;
179
+ $ddl$, data_schema);
180
+ END
181
+ $codex_execution_time_lease_guards$;
@@ -0,0 +1,471 @@
1
+ -- deployment-mode: maintenance
2
+ -- Opt-in OAuth 2.1-style authorization-server state for exact workspace MCP
3
+ -- resources. Only hashes of authorization codes and bearer/refresh tokens are
4
+ -- retained; grants freeze permissions and tool identities for later live
5
+ -- intersection.
6
+ -- This changes the exact runtime-posture table/grant contract. Stop every API,
7
+ -- control worker, and turn worker before applying it, and never restart a
8
+ -- pre-0404 image after commit.
9
+
10
+ SET LOCAL lock_timeout = '5s';
11
+ SET LOCAL statement_timeout = '5min';
12
+
13
+ DO $mcp_oauth_runtime_drain_before$
14
+ DECLARE
15
+ configured_roles_text text := nullif(
16
+ current_setting('opengeni.migration_application_roles', true), ''
17
+ );
18
+ configured_roles jsonb;
19
+ BEGIN
20
+ IF configured_roles_text IS NULL THEN
21
+ RAISE EXCEPTION
22
+ '0403 MCP OAuth activation requires an explicit application database role list'
23
+ USING ERRCODE = '55000';
24
+ END IF;
25
+ BEGIN
26
+ configured_roles := configured_roles_text::jsonb;
27
+ EXCEPTION WHEN OTHERS THEN
28
+ RAISE EXCEPTION
29
+ '0403 MCP OAuth activation received a malformed application database role list'
30
+ USING ERRCODE = '55000';
31
+ END;
32
+ IF jsonb_typeof(configured_roles) <> 'array'
33
+ OR jsonb_array_length(configured_roles) NOT BETWEEN 1 AND 16
34
+ OR EXISTS (
35
+ SELECT 1 FROM jsonb_array_elements(configured_roles) AS roles(value)
36
+ WHERE jsonb_typeof(value) <> 'string'
37
+ OR btrim(value #>> '{}') = ''
38
+ OR octet_length(value #>> '{}') > 63
39
+ )
40
+ OR (
41
+ SELECT count(*) FROM jsonb_array_elements_text(configured_roles)
42
+ ) <> (
43
+ SELECT count(DISTINCT value)
44
+ FROM jsonb_array_elements_text(configured_roles) AS roles(value)
45
+ )
46
+ THEN
47
+ RAISE EXCEPTION
48
+ '0403 MCP OAuth activation received an invalid application database role list'
49
+ USING ERRCODE = '55000';
50
+ END IF;
51
+ IF EXISTS (
52
+ SELECT 1 FROM pg_stat_activity activity
53
+ JOIN jsonb_array_elements_text(configured_roles) roles(role_name)
54
+ ON roles.role_name = activity.usename
55
+ WHERE activity.datname = current_database()
56
+ AND activity.pid <> pg_backend_pid()
57
+ )
58
+ THEN
59
+ RAISE EXCEPTION
60
+ '0403 MCP OAuth activation requires all configured OpenGeni application database sessions to be stopped'
61
+ USING ERRCODE = '55000';
62
+ END IF;
63
+ END
64
+ $mcp_oauth_runtime_drain_before$;
65
+
66
+ CREATE TABLE mcp_oauth_clients (
67
+ client_id text PRIMARY KEY CHECK (octet_length(client_id) BETWEEN 16 AND 256),
68
+ redirect_uris jsonb NOT NULL CHECK (
69
+ jsonb_typeof(redirect_uris) = 'array' AND jsonb_array_length(redirect_uris) BETWEEN 1 AND 16
70
+ ),
71
+ client_name text CHECK (client_name IS NULL OR octet_length(client_name) BETWEEN 1 AND 200),
72
+ token_endpoint_auth_method text NOT NULL DEFAULT 'none' CHECK (token_endpoint_auth_method = 'none'),
73
+ grant_types jsonb NOT NULL CHECK (jsonb_typeof(grant_types) = 'array'),
74
+ response_types jsonb NOT NULL CHECK (response_types = '["code"]'::jsonb),
75
+ registration_scope_hash text NOT NULL CHECK (registration_scope_hash ~ '^[0-9a-f]{64}$'),
76
+ created_at timestamptz NOT NULL DEFAULT now(),
77
+ expires_at timestamptz NOT NULL DEFAULT (now() + interval '1 day'),
78
+ CHECK (expires_at > created_at)
79
+ );
80
+ CREATE INDEX mcp_oauth_clients_created_idx ON mcp_oauth_clients(created_at);
81
+ CREATE INDEX mcp_oauth_clients_scope_created_idx
82
+ ON mcp_oauth_clients(registration_scope_hash, created_at);
83
+ CREATE INDEX mcp_oauth_clients_expires_idx ON mcp_oauth_clients(expires_at, client_id);
84
+
85
+ CREATE TABLE mcp_oauth_authorization_requests (
86
+ request_hash text PRIMARY KEY CHECK (request_hash ~ '^[0-9a-f]{64}$'),
87
+ client_id text NOT NULL REFERENCES mcp_oauth_clients(client_id) ON DELETE CASCADE,
88
+ account_id uuid NOT NULL REFERENCES managed_accounts(id) ON DELETE CASCADE,
89
+ workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
90
+ subject_id text NOT NULL CHECK (octet_length(subject_id) BETWEEN 1 AND 1024),
91
+ resource text NOT NULL CHECK (octet_length(resource) BETWEEN 1 AND 2048),
92
+ redirect_uri text NOT NULL CHECK (octet_length(redirect_uri) BETWEEN 1 AND 2048),
93
+ code_challenge text NOT NULL CHECK (code_challenge ~ '^[A-Za-z0-9_-]{43}$'),
94
+ state text CHECK (state IS NULL OR octet_length(state) BETWEEN 1 AND 1024),
95
+ permissions jsonb NOT NULL CHECK (jsonb_typeof(permissions) = 'array'),
96
+ tool_identities jsonb NOT NULL CHECK (
97
+ jsonb_typeof(tool_identities) = 'array' AND jsonb_array_length(tool_identities) <= 4096
98
+ ),
99
+ expires_at timestamptz NOT NULL,
100
+ created_at timestamptz NOT NULL DEFAULT now()
101
+ );
102
+ ALTER TABLE mcp_oauth_authorization_requests
103
+ ADD CONSTRAINT mcp_oauth_authorization_requests_workspace_account_fk
104
+ FOREIGN KEY (workspace_id, account_id) REFERENCES workspaces(id, account_id) ON DELETE CASCADE;
105
+ CREATE INDEX mcp_oauth_authorization_requests_expires_idx
106
+ ON mcp_oauth_authorization_requests(expires_at);
107
+
108
+ CREATE TABLE mcp_oauth_authorization_codes (
109
+ code_hash text PRIMARY KEY CHECK (code_hash ~ '^[0-9a-f]{64}$'),
110
+ client_id text NOT NULL REFERENCES mcp_oauth_clients(client_id) ON DELETE CASCADE,
111
+ account_id uuid NOT NULL REFERENCES managed_accounts(id) ON DELETE CASCADE,
112
+ workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
113
+ subject_id text NOT NULL CHECK (octet_length(subject_id) BETWEEN 1 AND 1024),
114
+ resource text NOT NULL CHECK (octet_length(resource) BETWEEN 1 AND 2048),
115
+ redirect_uri text NOT NULL CHECK (octet_length(redirect_uri) BETWEEN 1 AND 2048),
116
+ code_challenge text NOT NULL CHECK (code_challenge ~ '^[A-Za-z0-9_-]{43}$'),
117
+ permissions jsonb NOT NULL CHECK (jsonb_typeof(permissions) = 'array'),
118
+ tool_identities jsonb NOT NULL CHECK (
119
+ jsonb_typeof(tool_identities) = 'array' AND jsonb_array_length(tool_identities) <= 4096
120
+ ),
121
+ expires_at timestamptz NOT NULL,
122
+ created_at timestamptz NOT NULL DEFAULT now()
123
+ );
124
+ ALTER TABLE mcp_oauth_authorization_codes
125
+ ADD CONSTRAINT mcp_oauth_authorization_codes_workspace_account_fk
126
+ FOREIGN KEY (workspace_id, account_id) REFERENCES workspaces(id, account_id) ON DELETE CASCADE;
127
+ CREATE INDEX mcp_oauth_authorization_codes_expires_idx
128
+ ON mcp_oauth_authorization_codes(expires_at);
129
+
130
+ CREATE TABLE mcp_oauth_refresh_tokens (
131
+ token_hash text PRIMARY KEY CHECK (token_hash ~ '^[0-9a-f]{64}$'),
132
+ family_id uuid NOT NULL,
133
+ generation integer NOT NULL CHECK (generation > 0),
134
+ client_id text NOT NULL REFERENCES mcp_oauth_clients(client_id) ON DELETE CASCADE,
135
+ account_id uuid NOT NULL REFERENCES managed_accounts(id) ON DELETE CASCADE,
136
+ workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
137
+ subject_id text NOT NULL CHECK (octet_length(subject_id) BETWEEN 1 AND 1024),
138
+ resource text NOT NULL CHECK (octet_length(resource) BETWEEN 1 AND 2048),
139
+ permissions jsonb NOT NULL CHECK (jsonb_typeof(permissions) = 'array'),
140
+ tool_identities jsonb NOT NULL CHECK (
141
+ jsonb_typeof(tool_identities) = 'array' AND jsonb_array_length(tool_identities) <= 4096
142
+ ),
143
+ expires_at timestamptz NOT NULL,
144
+ revoked_at timestamptz,
145
+ created_at timestamptz NOT NULL DEFAULT now(),
146
+ UNIQUE (family_id, generation)
147
+ );
148
+ ALTER TABLE mcp_oauth_refresh_tokens
149
+ ADD CONSTRAINT mcp_oauth_refresh_tokens_workspace_account_fk
150
+ FOREIGN KEY (workspace_id, account_id) REFERENCES workspaces(id, account_id) ON DELETE CASCADE;
151
+ CREATE INDEX mcp_oauth_refresh_tokens_expires_idx ON mcp_oauth_refresh_tokens(expires_at);
152
+
153
+ CREATE TABLE mcp_oauth_access_tokens (
154
+ token_hash text PRIMARY KEY CHECK (token_hash ~ '^[0-9a-f]{64}$'),
155
+ refresh_family_id uuid NOT NULL,
156
+ refresh_generation integer NOT NULL CHECK (refresh_generation > 0),
157
+ client_id text NOT NULL REFERENCES mcp_oauth_clients(client_id) ON DELETE CASCADE,
158
+ account_id uuid NOT NULL REFERENCES managed_accounts(id) ON DELETE CASCADE,
159
+ workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
160
+ subject_id text NOT NULL CHECK (octet_length(subject_id) BETWEEN 1 AND 1024),
161
+ resource text NOT NULL CHECK (octet_length(resource) BETWEEN 1 AND 2048),
162
+ permissions jsonb NOT NULL CHECK (jsonb_typeof(permissions) = 'array'),
163
+ tool_identities jsonb NOT NULL CHECK (
164
+ jsonb_typeof(tool_identities) = 'array' AND jsonb_array_length(tool_identities) <= 4096
165
+ ),
166
+ expires_at timestamptz NOT NULL,
167
+ revoked_at timestamptz,
168
+ created_at timestamptz NOT NULL DEFAULT now()
169
+ );
170
+ ALTER TABLE mcp_oauth_access_tokens
171
+ ADD CONSTRAINT mcp_oauth_access_tokens_workspace_account_fk
172
+ FOREIGN KEY (workspace_id, account_id) REFERENCES workspaces(id, account_id) ON DELETE CASCADE;
173
+ CREATE INDEX mcp_oauth_access_tokens_family_idx
174
+ ON mcp_oauth_access_tokens(refresh_family_id, refresh_generation);
175
+ CREATE INDEX mcp_oauth_access_tokens_expires_idx ON mcp_oauth_access_tokens(expires_at);
176
+
177
+ -- Public dynamic client registration must remain bounded across every API
178
+ -- replica. Untouched registrations expire after one day; a successful client
179
+ -- lookup extends the client through the maximum refresh-token lifetime plus a
180
+ -- one-day grace period. The reaper is deliberately batch-bounded and is invoked
181
+ -- by registration and client lookup, so normal OAuth traffic continuously
182
+ -- removes expired protocol state without a separate scheduler.
183
+ CREATE FUNCTION opengeni_private.reap_mcp_oauth_state(p_limit integer DEFAULT 128)
184
+ RETURNS integer
185
+ LANGUAGE plpgsql SECURITY DEFINER
186
+ SET search_path FROM CURRENT
187
+ AS $body$
188
+ DECLARE
189
+ bounded_limit integer := least(greatest(coalesce(p_limit, 128), 1), 1000);
190
+ removed integer := 0;
191
+ affected integer;
192
+ BEGIN
193
+ WITH expired AS (
194
+ SELECT request_hash
195
+ FROM mcp_oauth_authorization_requests
196
+ WHERE expires_at <= pg_catalog.clock_timestamp()
197
+ ORDER BY expires_at, request_hash
198
+ FOR UPDATE SKIP LOCKED
199
+ LIMIT bounded_limit
200
+ )
201
+ DELETE FROM mcp_oauth_authorization_requests target
202
+ USING expired
203
+ WHERE target.request_hash = expired.request_hash;
204
+ GET DIAGNOSTICS affected = ROW_COUNT;
205
+ removed := removed + affected;
206
+
207
+ WITH expired AS (
208
+ SELECT code_hash
209
+ FROM mcp_oauth_authorization_codes
210
+ WHERE expires_at <= pg_catalog.clock_timestamp()
211
+ ORDER BY expires_at, code_hash
212
+ FOR UPDATE SKIP LOCKED
213
+ LIMIT bounded_limit
214
+ )
215
+ DELETE FROM mcp_oauth_authorization_codes target
216
+ USING expired
217
+ WHERE target.code_hash = expired.code_hash;
218
+ GET DIAGNOSTICS affected = ROW_COUNT;
219
+ removed := removed + affected;
220
+
221
+ WITH expired AS (
222
+ SELECT token_hash
223
+ FROM mcp_oauth_access_tokens
224
+ WHERE expires_at <= pg_catalog.clock_timestamp()
225
+ OR revoked_at <= pg_catalog.clock_timestamp() - interval '1 day'
226
+ ORDER BY expires_at, token_hash
227
+ FOR UPDATE SKIP LOCKED
228
+ LIMIT bounded_limit
229
+ )
230
+ DELETE FROM mcp_oauth_access_tokens target
231
+ USING expired
232
+ WHERE target.token_hash = expired.token_hash;
233
+ GET DIAGNOSTICS affected = ROW_COUNT;
234
+ removed := removed + affected;
235
+
236
+ WITH expired AS (
237
+ SELECT candidate.token_hash
238
+ FROM mcp_oauth_refresh_tokens candidate
239
+ WHERE (
240
+ candidate.expires_at <= pg_catalog.clock_timestamp()
241
+ OR candidate.revoked_at <= pg_catalog.clock_timestamp() - interval '1 day'
242
+ )
243
+ -- A rotated generation is the durable replay detector for its family.
244
+ -- Keep every tombstone until no descendant refresh token can remain live.
245
+ AND NOT EXISTS (
246
+ SELECT 1
247
+ FROM mcp_oauth_refresh_tokens live_family
248
+ WHERE live_family.family_id = candidate.family_id
249
+ AND live_family.revoked_at IS NULL
250
+ AND live_family.expires_at > pg_catalog.clock_timestamp()
251
+ )
252
+ ORDER BY candidate.expires_at, candidate.token_hash
253
+ FOR UPDATE SKIP LOCKED
254
+ LIMIT bounded_limit
255
+ )
256
+ DELETE FROM mcp_oauth_refresh_tokens target
257
+ USING expired
258
+ WHERE target.token_hash = expired.token_hash;
259
+ GET DIAGNOSTICS affected = ROW_COUNT;
260
+ removed := removed + affected;
261
+
262
+ WITH expired AS (
263
+ SELECT client_id
264
+ FROM mcp_oauth_clients
265
+ WHERE expires_at <= pg_catalog.clock_timestamp()
266
+ ORDER BY expires_at, client_id
267
+ FOR UPDATE SKIP LOCKED
268
+ LIMIT bounded_limit
269
+ )
270
+ DELETE FROM mcp_oauth_clients target
271
+ USING expired
272
+ WHERE target.client_id = expired.client_id;
273
+ GET DIAGNOSTICS affected = ROW_COUNT;
274
+ RETURN removed + affected;
275
+ END
276
+ $body$;
277
+
278
+ CREATE FUNCTION opengeni_private.register_mcp_oauth_client(
279
+ p_client_id text,
280
+ p_redirect_uris jsonb,
281
+ p_client_name text,
282
+ p_grant_types jsonb,
283
+ p_response_types jsonb,
284
+ p_registration_scope_hash text
285
+ )
286
+ RETURNS SETOF mcp_oauth_clients
287
+ LANGUAGE plpgsql SECURITY DEFINER
288
+ SET search_path FROM CURRENT
289
+ AS $body$
290
+ DECLARE
291
+ now_value timestamptz := pg_catalog.clock_timestamp();
292
+ global_count bigint;
293
+ scoped_count bigint;
294
+ BEGIN
295
+ IF p_registration_scope_hash !~ '^[0-9a-f]{64}$' THEN
296
+ RAISE EXCEPTION 'invalid MCP OAuth registration scope'
297
+ USING ERRCODE = '22023';
298
+ END IF;
299
+
300
+ -- One short global transaction lock makes the count-and-insert quota exact
301
+ -- across replicas. Registration is rare and the lock is held only for this
302
+ -- bounded cleanup/count/insert command.
303
+ PERFORM pg_catalog.pg_advisory_xact_lock(
304
+ pg_catalog.hashtextextended('opengeni:mcp-oauth:registration', 0)
305
+ );
306
+ PERFORM opengeni_private.reap_mcp_oauth_state(128);
307
+
308
+ SELECT count(*) INTO global_count
309
+ FROM mcp_oauth_clients
310
+ WHERE created_at > now_value - interval '10 minutes';
311
+
312
+ SELECT count(*) INTO scoped_count
313
+ FROM mcp_oauth_clients
314
+ WHERE registration_scope_hash = p_registration_scope_hash
315
+ AND created_at > now_value - interval '10 minutes';
316
+
317
+ IF global_count >= 600 OR scoped_count >= 20 THEN
318
+ RAISE EXCEPTION 'MCP OAuth client registration rate limited'
319
+ USING ERRCODE = 'P0004';
320
+ END IF;
321
+
322
+ RETURN QUERY
323
+ INSERT INTO mcp_oauth_clients (
324
+ client_id, redirect_uris, client_name, grant_types, response_types,
325
+ registration_scope_hash, expires_at
326
+ ) VALUES (
327
+ p_client_id, p_redirect_uris, p_client_name, p_grant_types, p_response_types,
328
+ p_registration_scope_hash, now_value + interval '1 day'
329
+ )
330
+ RETURNING *;
331
+ END
332
+ $body$;
333
+
334
+ REVOKE ALL ON mcp_oauth_clients, mcp_oauth_authorization_requests,
335
+ mcp_oauth_authorization_codes, mcp_oauth_refresh_tokens, mcp_oauth_access_tokens FROM PUBLIC;
336
+ REVOKE ALL ON FUNCTION opengeni_private.reap_mcp_oauth_state(integer) FROM PUBLIC;
337
+ REVOKE ALL ON FUNCTION opengeni_private.register_mcp_oauth_client(
338
+ text, jsonb, text, jsonb, jsonb, text
339
+ ) FROM PUBLIC;
340
+
341
+ -- Owner ALTER DEFAULT PRIVILEGES may still name a legacy application login or
342
+ -- an old side of a role rotation. The exact role list above is drain detection
343
+ -- only. Strip every explicit non-owner ACL from the new objects; the mandatory
344
+ -- post-migration db:provision-roles step grants only the deployment's current
345
+ -- target application role.
346
+ DO $mcp_oauth_table_acl_reset$
347
+ DECLARE
348
+ data_schema text := pg_catalog.current_schema();
349
+ relation_name text;
350
+ role_name text;
351
+ BEGIN
352
+ FOR relation_name IN
353
+ SELECT relation.value
354
+ FROM pg_catalog.unnest(
355
+ ARRAY[
356
+ 'mcp_oauth_clients',
357
+ 'mcp_oauth_authorization_requests',
358
+ 'mcp_oauth_authorization_codes',
359
+ 'mcp_oauth_refresh_tokens',
360
+ 'mcp_oauth_access_tokens'
361
+ ]::text[]
362
+ ) AS relation(value)
363
+ LOOP
364
+ EXECUTE pg_catalog.format(
365
+ 'REVOKE ALL ON TABLE %I.%I FROM PUBLIC',
366
+ data_schema,
367
+ relation_name
368
+ );
369
+ FOR role_name IN
370
+ SELECT grantee_role.rolname
371
+ FROM pg_catalog.pg_class relation
372
+ CROSS JOIN LATERAL pg_catalog.aclexplode(
373
+ coalesce(
374
+ relation.relacl,
375
+ pg_catalog.acldefault('r', relation.relowner)
376
+ )
377
+ ) privilege
378
+ INNER JOIN pg_catalog.pg_roles grantee_role
379
+ ON grantee_role.oid = privilege.grantee
380
+ WHERE relation.oid = pg_catalog.to_regclass(
381
+ pg_catalog.format('%I.%I', data_schema, relation_name)
382
+ )
383
+ AND privilege.grantee <> 0
384
+ AND privilege.grantee <> relation.relowner
385
+ GROUP BY grantee_role.rolname
386
+ ORDER BY grantee_role.rolname COLLATE "C"
387
+ LOOP
388
+ EXECUTE pg_catalog.format(
389
+ 'REVOKE ALL ON TABLE %I.%I FROM %I',
390
+ data_schema,
391
+ relation_name,
392
+ role_name
393
+ );
394
+ END LOOP;
395
+ END LOOP;
396
+ END
397
+ $mcp_oauth_table_acl_reset$;
398
+
399
+ DO $mcp_oauth_function_acl_reset$
400
+ DECLARE
401
+ role_name text;
402
+ target record;
403
+ BEGIN
404
+ FOR target IN
405
+ SELECT *
406
+ FROM (VALUES
407
+ (
408
+ 'reap_mcp_oauth_state'::text,
409
+ 'p_limit integer'::text,
410
+ 'integer'::text
411
+ ),
412
+ (
413
+ 'register_mcp_oauth_client'::text,
414
+ 'p_client_id text, p_redirect_uris jsonb, p_client_name text, p_grant_types jsonb, p_response_types jsonb, p_registration_scope_hash text'::text,
415
+ 'text, jsonb, text, jsonb, jsonb, text'::text
416
+ )
417
+ ) target_row(function_name, identity_arguments, call_arguments)
418
+ LOOP
419
+ FOR role_name IN
420
+ SELECT grantee_role.rolname
421
+ FROM pg_catalog.pg_proc procedure
422
+ INNER JOIN pg_catalog.pg_namespace namespace
423
+ ON namespace.oid = procedure.pronamespace
424
+ CROSS JOIN LATERAL pg_catalog.aclexplode(
425
+ coalesce(
426
+ procedure.proacl,
427
+ pg_catalog.acldefault('f', procedure.proowner)
428
+ )
429
+ ) privilege
430
+ INNER JOIN pg_catalog.pg_roles grantee_role
431
+ ON grantee_role.oid = privilege.grantee
432
+ WHERE namespace.nspname = 'opengeni_private'
433
+ AND procedure.proname = target.function_name
434
+ AND pg_catalog.pg_get_function_identity_arguments(procedure.oid) =
435
+ target.identity_arguments
436
+ AND privilege.grantee <> 0
437
+ AND privilege.grantee <> procedure.proowner
438
+ GROUP BY grantee_role.rolname
439
+ ORDER BY grantee_role.rolname COLLATE "C"
440
+ LOOP
441
+ EXECUTE pg_catalog.format(
442
+ 'REVOKE ALL ON FUNCTION opengeni_private.%I(%s) FROM %I',
443
+ target.function_name,
444
+ target.call_arguments,
445
+ role_name
446
+ );
447
+ END LOOP;
448
+ END LOOP;
449
+ END
450
+ $mcp_oauth_function_acl_reset$;
451
+
452
+ DO $mcp_oauth_runtime_drain_after$
453
+ DECLARE
454
+ configured_roles jsonb := current_setting(
455
+ 'opengeni.migration_application_roles', false
456
+ )::jsonb;
457
+ BEGIN
458
+ IF EXISTS (
459
+ SELECT 1 FROM pg_stat_activity activity
460
+ JOIN jsonb_array_elements_text(configured_roles) roles(role_name)
461
+ ON roles.role_name = activity.usename
462
+ WHERE activity.datname = current_database()
463
+ AND activity.pid <> pg_backend_pid()
464
+ )
465
+ THEN
466
+ RAISE EXCEPTION
467
+ '0403 MCP OAuth activation observed a configured OpenGeni application database session after schema installation'
468
+ USING ERRCODE = '55000';
469
+ END IF;
470
+ END
471
+ $mcp_oauth_runtime_drain_after$;