@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,184 @@
1
+ -- deployment-mode: maintenance
2
+ -- Current-human HTTP tool calls use hash-only, single-use approval capabilities
3
+ -- instead of trusting a caller-provided approval boolean.
4
+ -- This changes the exact runtime-posture table/grant/RLS contract. Stop every
5
+ -- API, control worker, and turn worker before applying it, and never restart a
6
+ -- pre-0404 image after commit.
7
+
8
+ SET LOCAL lock_timeout = '5s';
9
+ SET LOCAL statement_timeout = '5min';
10
+
11
+ DO $tool_gateway_approval_runtime_drain_before$
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
+ '0404 tool gateway approval activation 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
+ '0404 tool gateway approval activation 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 octet_length(value #>> '{}') > 63
37
+ )
38
+ OR (
39
+ SELECT count(*) FROM jsonb_array_elements_text(configured_roles)
40
+ ) <> (
41
+ SELECT count(DISTINCT value)
42
+ FROM jsonb_array_elements_text(configured_roles) AS roles(value)
43
+ )
44
+ THEN
45
+ RAISE EXCEPTION
46
+ '0404 tool gateway approval activation received an invalid application database role list'
47
+ USING ERRCODE = '55000';
48
+ END IF;
49
+ IF EXISTS (
50
+ SELECT 1 FROM pg_stat_activity activity
51
+ JOIN jsonb_array_elements_text(configured_roles) roles(role_name)
52
+ ON roles.role_name = activity.usename
53
+ WHERE activity.datname = current_database()
54
+ AND activity.pid <> pg_backend_pid()
55
+ )
56
+ THEN
57
+ RAISE EXCEPTION
58
+ '0404 tool gateway approval activation requires all configured OpenGeni application database sessions to be stopped'
59
+ USING ERRCODE = '55000';
60
+ END IF;
61
+ END
62
+ $tool_gateway_approval_runtime_drain_before$;
63
+
64
+ CREATE TABLE "tool_gateway_approval_capabilities" (
65
+ "token_hash" text PRIMARY KEY,
66
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE CASCADE,
67
+ "workspace_id" uuid NOT NULL REFERENCES "workspaces"("id") ON DELETE CASCADE,
68
+ "subject_id" text NOT NULL,
69
+ "operation_id" uuid NOT NULL,
70
+ "catalog_digest" text NOT NULL,
71
+ "server_id" text NOT NULL,
72
+ "tool_name" text NOT NULL,
73
+ "arguments_digest" text NOT NULL,
74
+ "authority_digest" text NOT NULL,
75
+ "expires_at" timestamptz NOT NULL,
76
+ "consumed_at" timestamptz,
77
+ "created_at" timestamptz NOT NULL DEFAULT now(),
78
+ CONSTRAINT "tool_gateway_approval_capabilities_workspace_account_fk"
79
+ FOREIGN KEY ("workspace_id", "account_id")
80
+ REFERENCES "workspaces"("id", "account_id") ON DELETE CASCADE,
81
+ CONSTRAINT "tool_gateway_approval_capabilities_token_hash_chk"
82
+ CHECK ("token_hash" ~ '^[0-9a-f]{64}$'),
83
+ CONSTRAINT "tool_gateway_approval_capabilities_catalog_digest_chk"
84
+ CHECK ("catalog_digest" ~ '^[0-9a-f]{64}$'),
85
+ CONSTRAINT "tool_gateway_approval_capabilities_arguments_digest_chk"
86
+ CHECK ("arguments_digest" ~ '^[0-9a-f]{64}$'),
87
+ CONSTRAINT "tool_gateway_approval_capabilities_authority_digest_chk"
88
+ CHECK ("authority_digest" ~ '^[0-9a-f]{64}$'),
89
+ CONSTRAINT "tool_gateway_approval_capabilities_subject_chk"
90
+ CHECK (length(btrim("subject_id")) BETWEEN 1 AND 1024),
91
+ CONSTRAINT "tool_gateway_approval_capabilities_identity_chk"
92
+ CHECK (
93
+ length("server_id") BETWEEN 1 AND 256
94
+ AND length("tool_name") BETWEEN 1 AND 512
95
+ ),
96
+ CONSTRAINT "tool_gateway_approval_capabilities_expiry_chk"
97
+ CHECK ("expires_at" > "created_at" AND "expires_at" <= "created_at" + interval '10 minutes')
98
+ );
99
+
100
+ CREATE UNIQUE INDEX "tool_gateway_approval_capabilities_operation_uq"
101
+ ON "tool_gateway_approval_capabilities" ("workspace_id", "subject_id", "operation_id");
102
+ CREATE INDEX "tool_gateway_approval_capabilities_expires_idx"
103
+ ON "tool_gateway_approval_capabilities" ("expires_at", "token_hash");
104
+ CREATE INDEX "tool_gateway_approval_capabilities_live_subject_expiry_idx"
105
+ ON "tool_gateway_approval_capabilities" (
106
+ "workspace_id", "subject_id", "expires_at", "token_hash"
107
+ )
108
+ WHERE "consumed_at" IS NULL;
109
+
110
+ ALTER TABLE "tool_gateway_approval_capabilities" ENABLE ROW LEVEL SECURITY;
111
+ ALTER TABLE "tool_gateway_approval_capabilities" FORCE ROW LEVEL SECURITY;
112
+ CREATE POLICY "workspace_subject_isolation" ON "tool_gateway_approval_capabilities"
113
+ USING (
114
+ opengeni_private.workspace_rls_visible("account_id", "workspace_id")
115
+ AND "subject_id" = opengeni_private.current_subject_id()
116
+ )
117
+ WITH CHECK (
118
+ opengeni_private.workspace_rls_visible("account_id", "workspace_id")
119
+ AND "subject_id" = opengeni_private.current_subject_id()
120
+ );
121
+
122
+ REVOKE ALL ON "tool_gateway_approval_capabilities" FROM PUBLIC;
123
+
124
+ -- The application role list is drain detection only. Remove any explicit ACL
125
+ -- inherited from owner default privileges, including an old side of a runtime
126
+ -- role rotation. The post-migration role provisioner grants only the exact
127
+ -- current target application role.
128
+ DO $tool_gateway_approval_table_acl_reset$
129
+ DECLARE
130
+ data_schema text := pg_catalog.current_schema();
131
+ role_name text;
132
+ BEGIN
133
+ EXECUTE pg_catalog.format(
134
+ 'REVOKE ALL ON TABLE %I.tool_gateway_approval_capabilities FROM PUBLIC',
135
+ data_schema
136
+ );
137
+ FOR role_name IN
138
+ SELECT grantee_role.rolname
139
+ FROM pg_catalog.pg_class relation
140
+ CROSS JOIN LATERAL pg_catalog.aclexplode(
141
+ coalesce(
142
+ relation.relacl,
143
+ pg_catalog.acldefault('r', relation.relowner)
144
+ )
145
+ ) privilege
146
+ INNER JOIN pg_catalog.pg_roles grantee_role
147
+ ON grantee_role.oid = privilege.grantee
148
+ WHERE relation.oid = pg_catalog.to_regclass(
149
+ pg_catalog.format('%I.tool_gateway_approval_capabilities', data_schema)
150
+ )
151
+ AND privilege.grantee <> 0
152
+ AND privilege.grantee <> relation.relowner
153
+ GROUP BY grantee_role.rolname
154
+ ORDER BY grantee_role.rolname COLLATE "C"
155
+ LOOP
156
+ EXECUTE pg_catalog.format(
157
+ 'REVOKE ALL ON TABLE %I.tool_gateway_approval_capabilities FROM %I',
158
+ data_schema,
159
+ role_name
160
+ );
161
+ END LOOP;
162
+ END
163
+ $tool_gateway_approval_table_acl_reset$;
164
+
165
+ DO $tool_gateway_approval_runtime_drain_after$
166
+ DECLARE
167
+ configured_roles jsonb := current_setting(
168
+ 'opengeni.migration_application_roles', false
169
+ )::jsonb;
170
+ BEGIN
171
+ IF EXISTS (
172
+ SELECT 1 FROM pg_stat_activity activity
173
+ JOIN jsonb_array_elements_text(configured_roles) roles(role_name)
174
+ ON roles.role_name = activity.usename
175
+ WHERE activity.datname = current_database()
176
+ AND activity.pid <> pg_backend_pid()
177
+ )
178
+ THEN
179
+ RAISE EXCEPTION
180
+ '0404 tool gateway approval activation observed a configured OpenGeni application database session after schema installation'
181
+ USING ERRCODE = '55000';
182
+ END IF;
183
+ END
184
+ $tool_gateway_approval_runtime_drain_after$;
@@ -0,0 +1,71 @@
1
+ -- deployment-mode: rolling
2
+ -- Retain editable source and exact requested-tool identities beside each
3
+ -- immutable Workspace HTML artifact version. Existing versions remain valid:
4
+ -- their source columns stay NULL and readers derive a one-file index.html
5
+ -- source bundle from the already-verified published HTML.
6
+
7
+ SET LOCAL lock_timeout = '5s';
8
+ SET LOCAL statement_timeout = '5min';
9
+
10
+ ALTER TABLE "workspace_artifact_versions"
11
+ ADD COLUMN "source_key" text,
12
+ ADD COLUMN "source_sha256" text,
13
+ ADD COLUMN "source_size_bytes" integer,
14
+ ADD COLUMN "requested_tools" jsonb NOT NULL DEFAULT '[]'::jsonb;
15
+
16
+ ALTER TABLE "workspace_artifact_versions"
17
+ DROP CONSTRAINT "workspace_artifact_versions_content_chk";
18
+
19
+ ALTER TABLE "workspace_artifact_versions"
20
+ ADD CONSTRAINT "workspace_artifact_versions_content_chk" CHECK (
21
+ "content_type" = 'text/html'
22
+ AND "content_sha256" ~ '^[0-9a-f]{64}$'
23
+ AND "size_bytes" BETWEEN 1 AND 4194304
24
+ AND length("content_key") BETWEEN 1 AND 1024
25
+ AND (
26
+ (
27
+ "source_key" IS NULL
28
+ AND "source_sha256" IS NULL
29
+ AND "source_size_bytes" IS NULL
30
+ ) OR (
31
+ "source_key" IS NOT NULL
32
+ AND "source_sha256" IS NOT NULL
33
+ AND "source_size_bytes" IS NOT NULL
34
+ AND
35
+ length("source_key") BETWEEN 1 AND 1024
36
+ AND "source_sha256" ~ '^[0-9a-f]{64}$'
37
+ AND "source_size_bytes" BETWEEN 1 AND 4194304
38
+ )
39
+ )
40
+ AND jsonb_typeof("requested_tools") = 'array'
41
+ AND jsonb_array_length("requested_tools") <= 128
42
+ ) NOT VALID;
43
+
44
+ ALTER TABLE "workspace_artifact_versions"
45
+ VALIDATE CONSTRAINT "workspace_artifact_versions_content_chk";
46
+
47
+ ALTER TABLE "workspace_artifact_events"
48
+ ADD COLUMN "request_digest" text;
49
+
50
+ ALTER TABLE "workspace_artifact_events"
51
+ ADD CONSTRAINT "workspace_artifact_events_request_digest_chk"
52
+ CHECK (
53
+ "request_digest" IS NULL
54
+ OR "request_digest" ~ '^[0-9a-f]{64}$'
55
+ ) NOT VALID;
56
+
57
+ ALTER TABLE "workspace_artifact_events"
58
+ VALIDATE CONSTRAINT "workspace_artifact_events_request_digest_chk";
59
+
60
+ ALTER TABLE "workspace_artifact_events"
61
+ DROP CONSTRAINT "workspace_artifact_events_type_chk";
62
+
63
+ ALTER TABLE "workspace_artifact_events"
64
+ ADD CONSTRAINT "workspace_artifact_events_type_chk"
65
+ CHECK ("type" IN ('published', 'rolled_back', 'archived', 'restored')) NOT VALID;
66
+
67
+ ALTER TABLE "workspace_artifact_events"
68
+ VALIDATE CONSTRAINT "workspace_artifact_events_type_chk";
69
+
70
+ CREATE INDEX "workspace_artifacts_status_list_idx"
71
+ ON "workspace_artifacts" ("workspace_id", "status", "updated_at");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/db",
3
- "version": "3.7.4",
3
+ "version": "4.0.0-canary.0",
4
4
  "description": "OpenGeni persistence: Drizzle schema, RLS-scoped query layer, the SQL migration runner, and role provisioning.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -78,11 +78,11 @@
78
78
  "prepublishOnly": "bash ../../scripts/prepublish-guard"
79
79
  },
80
80
  "dependencies": {
81
- "@opengeni/codemode": "^0.4.22",
82
- "@opengeni/codex": "^0.2.20",
83
- "@opengeni/config": "^0.22.5",
84
- "@opengeni/contracts": "^2.9.2",
85
- "@opengeni/network": "^0.2.3",
81
+ "@opengeni/codemode": "^0.4.27-canary.0",
82
+ "@opengeni/codex": "^0.2.21-canary.0",
83
+ "@opengeni/config": "^1.0.0-canary.0",
84
+ "@opengeni/contracts": "^2.13.0-canary.0",
85
+ "@opengeni/network": "^0.3.0-canary.0",
86
86
  "drizzle-orm": "^0.45.2",
87
87
  "postgres": "^3.4.7"
88
88
  },
@@ -223,6 +223,8 @@ export async function createAutomationTrigger(
223
223
  createdBySubjectId: string;
224
224
  request: CreateAutomationTriggerRequest;
225
225
  adapterId: string;
226
+ /** Trusted database-only admission seam. Throwing rolls creation back. */
227
+ beforeCreateCommit?: (tx: Database) => Promise<void>;
226
228
  },
227
229
  ): Promise<AutomationTrigger> {
228
230
  return await withWorkspaceRls(
@@ -289,6 +291,7 @@ export async function createAutomationTrigger(
289
291
  throw new Error("Automation Pack trigger must use its frozen manifest template");
290
292
  }
291
293
  }
294
+ await input.beforeCreateCommit?.(tx);
292
295
  const [head] = await tx
293
296
  .insert(schema.automationTriggers)
294
297
  .values({
@@ -438,6 +441,8 @@ export async function updateAutomationTrigger(
438
441
  triggerId: string;
439
442
  subjectId: string;
440
443
  request: UpdateAutomationTriggerRequest;
444
+ /** Trusted database-only admission seam. Throwing rolls the revision back. */
445
+ beforeUpdateCommit?: (tx: Database) => Promise<void>;
441
446
  },
442
447
  ): Promise<AutomationTrigger | null> {
443
448
  return await withWorkspaceRls(
@@ -478,6 +483,7 @@ export async function updateAutomationTrigger(
478
483
  "Pack-owned automation triggers must be managed through their Pack setup API",
479
484
  );
480
485
  }
486
+ await input.beforeUpdateCommit?.(tx);
481
487
  const revisionNumber = existing.head.currentRevision + 1;
482
488
  const [head] = await tx
483
489
  .update(schema.automationTriggers)
@@ -696,7 +702,9 @@ export async function claimAutomationRun(
696
702
  .for("update")
697
703
  .limit(1);
698
704
  if (!row) return null;
699
- if (row.status === "dispatched" || row.status === "skipped") return mapRunExecution(row);
705
+ if (row.status === "dispatched" || row.status === "skipped" || row.status === "failed") {
706
+ return mapRunExecution(row);
707
+ }
700
708
  const [updated] = await tx
701
709
  .update(schema.automationRuns)
702
710
  .set({
@@ -176,6 +176,8 @@ export type ApiIntegrationRuntime = {
176
176
  providerDomain: string;
177
177
  authScheme: Record<string, unknown>;
178
178
  connectionRef: McpServerConnectionRef | null;
179
+ /** Live credential authority revision used to invalidate stale human approvals. */
180
+ connectionAuthorityGeneration: number | null;
179
181
  allowedTools: string[];
180
182
  requireApproval: true | string[];
181
183
  revision: StoredApiIntegrationRevision;
@@ -843,6 +845,7 @@ export async function listInstalledApiIntegrationsInRlsContext(
843
845
  runtimeKey: schema.integrationFacetBindings.runtimeKey,
844
846
  bindingConfig: schema.integrationFacetBindings.config,
845
847
  connectionId: schema.integrationFacetBindings.connectionId,
848
+ connectionAuthorityGeneration: schema.connections.authorityGeneration,
846
849
  revision: schema.integrationSpecRevisions.spec,
847
850
  })
848
851
  .from(schema.integrationFacetBindings)
@@ -890,6 +893,10 @@ export async function listInstalledApiIntegrationsInRlsContext(
890
893
  eq(schema.integrationSpecRevisions.status, "active"),
891
894
  ),
892
895
  )
896
+ .leftJoin(
897
+ schema.connections,
898
+ eq(schema.connections.id, schema.integrationFacetBindings.connectionId),
899
+ )
893
900
  .where(
894
901
  and(
895
902
  eq(schema.capabilityPluginInstallations.workspaceId, workspaceId),
@@ -998,6 +1005,7 @@ export async function listInstalledApiIntegrationsInRlsContext(
998
1005
  providerDomain: row.providerDomain,
999
1006
  authScheme: objectValue(row.authScheme),
1000
1007
  connectionRef,
1008
+ connectionAuthorityGeneration: row.connectionAuthorityGeneration,
1001
1009
  allowedTools,
1002
1010
  requireApproval,
1003
1011
  revision,
@@ -92,6 +92,14 @@ export async function submitCodemodeOperation(
92
92
  { accountId: input.accountId, workspaceId: input.workspaceId },
93
93
  async (scopedDb) =>
94
94
  await scopedDb.transaction(async (tx) => {
95
+ // Serialize the caller-owned id before testing for an existing row.
96
+ // Without this fence, two first submissions can both observe absence
97
+ // and the later insert fails its unique constraint instead of replaying.
98
+ await tx.execute(sql`
99
+ select pg_advisory_xact_lock(
100
+ hashtextextended(${`codemode-operation:${input.workspaceId}:${call.operationId}`}, 0)
101
+ )
102
+ `);
95
103
  const [existing] = await tx
96
104
  .select()
97
105
  .from(schema.sessionAttemptCodemodeCalls)
@@ -213,7 +221,13 @@ export async function getCodemodeOperation(
213
221
  }
214
222
 
215
223
  export type ClaimCodemodeOperationResult =
216
- | { status: "claimed"; operation: CodemodeOperationValue; claimId: string }
224
+ | {
225
+ status: "claimed";
226
+ operation: CodemodeOperationValue;
227
+ claimId: string;
228
+ reclaimed: boolean;
229
+ }
230
+ | { status: "execution_owner_lost"; operation: CodemodeOperationValue; claimId: string }
217
231
  | { status: "already_running" | "terminal"; operation: CodemodeOperationValue }
218
232
  | { status: "rejected"; operation: CodemodeOperationValue | null };
219
233
 
@@ -250,30 +264,20 @@ export async function claimCodemodeOperation(
250
264
  }
251
265
  const now = input.now ?? new Date();
252
266
  const claimExpiresAt = new Date(now.getTime() + boundedClaimLeaseMs(input.claimLeaseMs));
267
+ let reclaimed = false;
253
268
  if (row.state === "running") {
254
269
  if (!row.claimExpiresAt || row.claimExpiresAt.getTime() > now.getTime()) {
255
270
  return { status: "already_running", operation: mapOperation(row) };
256
271
  }
257
272
  if (row.executionStartedAt) {
258
- const [unknown] = await tx
259
- .update(schema.sessionAttemptCodemodeCalls)
260
- .set({
261
- state: "outcome_unknown",
262
- errorCode: "worker_lost_during_execution",
263
- errorMessage:
264
- "The execution owner disappeared after the tool call began. Inspect actual state before retrying.",
265
- completedAt: now,
266
- updatedAt: now,
267
- })
268
- .where(
269
- and(
270
- eq(schema.sessionAttemptCodemodeCalls.operationId, input.operationId),
271
- eq(schema.sessionAttemptCodemodeCalls.state, "running"),
272
- eq(schema.sessionAttemptCodemodeCalls.claimId, row.claimId!),
273
- ),
274
- )
275
- .returning();
276
- return { status: "terminal", operation: mapOperation(unknown ?? row) };
273
+ if (!row.claimId) {
274
+ throw new Error("Codemode running operation is missing its claim id");
275
+ }
276
+ return {
277
+ status: "execution_owner_lost",
278
+ operation: mapOperation(row),
279
+ claimId: row.claimId,
280
+ };
277
281
  }
278
282
  const [requeued] = await tx
279
283
  .update(schema.sessionAttemptCodemodeCalls)
@@ -294,6 +298,7 @@ export async function claimCodemodeOperation(
294
298
  .returning();
295
299
  if (!requeued) throw new Error("Codemode expired claim recovery lost its row lock");
296
300
  row = requeued;
301
+ reclaimed = true;
297
302
  }
298
303
  if (isTerminalState(row.state)) return { status: "terminal", operation: mapOperation(row) };
299
304
 
@@ -353,7 +358,12 @@ export async function claimCodemodeOperation(
353
358
  )
354
359
  .returning();
355
360
  if (!claimed) throw new Error("Codemode operation claim lost its row lock");
356
- return { status: "claimed", operation: mapOperation(claimed), claimId: input.claimId };
361
+ return {
362
+ status: "claimed",
363
+ operation: mapOperation(claimed),
364
+ claimId: input.claimId,
365
+ reclaimed,
366
+ };
357
367
  }),
358
368
  );
359
369
  }
@@ -101,7 +101,12 @@ type CodexCredentialStatusTarget = {
101
101
  // onto the OLD in-flight refresh and writing stale rotated tokens over the freshly
102
102
  // connected credential. Concurrent calls for the SAME credential still coalesce,
103
103
  // so the one-time refresh token is never double-spent.
104
- const inflight = new Map<string, Promise<CodexTokenSnapshot>>();
104
+ export type CodexCredentialTokenSnapshot = CodexTokenSnapshot & {
105
+ /** Exact credential-row version whose bearer is about to reach the provider. */
106
+ credentialVersion: number;
107
+ };
108
+
109
+ const inflight = new Map<string, Promise<CodexCredentialTokenSnapshot>>();
105
110
  const CODEX_TOKEN_REFRESH_TIMEOUT_MS = 6_000;
106
111
 
107
112
  export type CodexTokenDeadlineClock = {
@@ -235,17 +240,21 @@ export function buildCodexTokenResolver(
235
240
  // clobber the newly-active account. The single-flight map needs zero change.
236
241
  credentialId: string,
237
242
  deps: CodexAuthDeps,
238
- ): { getToken: () => Promise<CodexTokenSnapshot>; refresh: () => Promise<CodexTokenSnapshot> } {
239
- const snapshot = (cred: CodexCredentialForRun): CodexTokenSnapshot => ({
243
+ ): {
244
+ getToken: () => Promise<CodexCredentialTokenSnapshot>;
245
+ refresh: () => Promise<CodexCredentialTokenSnapshot>;
246
+ } {
247
+ const snapshot = (cred: CodexCredentialForRun): CodexCredentialTokenSnapshot => ({
240
248
  accessToken: cred.tokens.accessToken,
241
249
  chatgptAccountId: cred.chatgptAccountId,
242
250
  isFedramp: cred.isFedramp,
251
+ credentialVersion: cred.version,
243
252
  });
244
253
 
245
254
  const performRefresh = async (
246
255
  refreshDb: Database,
247
256
  cred: CodexCredentialForRun,
248
- ): Promise<CodexTokenSnapshot> => {
257
+ ): Promise<CodexCredentialTokenSnapshot> => {
249
258
  try {
250
259
  // Bound even injected/custom refresh implementations that ignore abort
251
260
  // signals. The provider client has its own AbortController timeout; this
@@ -287,6 +296,7 @@ export function buildCodexTokenResolver(
287
296
  accessToken: tokens.access_token,
288
297
  chatgptAccountId: cred.chatgptAccountId,
289
298
  isFedramp: cred.isFedramp,
299
+ credentialVersion: cred.version + 1,
290
300
  };
291
301
  } catch (error) {
292
302
  if (error instanceof CodexReloginRequired) {
@@ -305,7 +315,7 @@ export function buildCodexTokenResolver(
305
315
 
306
316
  // ALL refreshes — whether proactive or a 401 retry — coalesce locally and then
307
317
  // serialize globally before any rotating refresh token reaches the provider.
308
- const doRefresh = (cred: CodexCredentialForRun): Promise<CodexTokenSnapshot> => {
318
+ const doRefresh = (cred: CodexCredentialForRun): Promise<CodexCredentialTokenSnapshot> => {
309
319
  const key = `${cred.id}:${cred.version}`;
310
320
  const existing = inflight.get(key);
311
321
  if (existing) {
@@ -346,7 +356,7 @@ export function buildCodexTokenResolver(
346
356
  return promise;
347
357
  };
348
358
 
349
- const resolve = async (force: boolean): Promise<CodexTokenSnapshot> => {
359
+ const resolve = async (force: boolean): Promise<CodexCredentialTokenSnapshot> => {
350
360
  const cred = await deps.loadCredential(db, settings, workspaceId, credentialId);
351
361
  if (!cred) {
352
362
  throw new CodexReloginRequired("No Codex subscription is connected for this workspace.");
@@ -114,6 +114,8 @@ export type ResolveConnectionCredentialResult =
114
114
  | {
115
115
  status: "ok";
116
116
  headers: Record<string, string>;
117
+ /** Credential authority is owned by the embedding host, not the native connection store. */
118
+ authoritySource?: "host";
117
119
  /** Present when the credential bundle or embedding host supplied explicit placements. */
118
120
  placements?: ConnectionCredentialPlacement[];
119
121
  connectionId: string;
@@ -133,6 +135,8 @@ export type ResolveConnectionCredentialResult =
133
135
  status: "auth_needed";
134
136
  reason: McpCredentialAuthNeededReason;
135
137
  providerDomain: string;
138
+ /** Credential authority is owned by the embedding host, not the native connection store. */
139
+ authoritySource?: "host";
136
140
  provider?: string;
137
141
  connectionId?: string;
138
142
  scopes?: string[];
@@ -158,6 +162,10 @@ export type ResolveConnectionCredentialInput = {
158
162
  /** Defaults to header-only MCP transport. */
159
163
  credentialTarget?: "mcp" | "http_api";
160
164
  forceRefresh?: boolean;
165
+ /** Internal lookup mode. Preflight must not refresh credentials or record provider usage. */
166
+ credentialResolutionMode?: "execution" | "preflight";
167
+ /** Frozen provider authority generation captured by the calling integration/catalog. */
168
+ expectedAuthorityGeneration?: number;
161
169
  /** Exact immutable accepted-work authority; never credential-bearing. */
162
170
  connectionUseAuthority?: unknown;
163
171
  /** Exact accepted attempt plus one stable physical-provider request id. */
@@ -270,6 +278,9 @@ export function buildHostConnectionTokenResolver(
270
278
  serverId: input.serverId,
271
279
  connectionRef: {
272
280
  providerDomain: input.connectionRef.providerDomain,
281
+ ...(input.connectionRef.authoritySource
282
+ ? { authoritySource: input.connectionRef.authoritySource }
283
+ : {}),
273
284
  ...(input.connectionRef.provider ? { provider: input.connectionRef.provider } : {}),
274
285
  ...(input.connectionRef.connectionId
275
286
  ? { connectionId: input.connectionRef.connectionId }
@@ -303,6 +314,7 @@ export function buildHostConnectionTokenResolver(
303
314
  status: "auth_needed",
304
315
  reason: result.reason,
305
316
  providerDomain: result.providerDomain,
317
+ authoritySource: "host",
306
318
  ...(result.provider ? { provider: result.provider } : {}),
307
319
  ...(result.connectionId ? { connectionId: result.connectionId } : {}),
308
320
  ...(result.scopes ? { scopes: [...result.scopes] } : {}),
@@ -339,6 +351,7 @@ export function buildHostConnectionTokenResolver(
339
351
  return {
340
352
  status: "ok",
341
353
  headers,
354
+ authoritySource: "host",
342
355
  ...(explicitPlacements ? { placements: explicitPlacements } : {}),
343
356
  connectionId: result.connectionId,
344
357
  ...(expiresAt !== undefined ? { expiresAt } : {}),
@@ -712,6 +725,7 @@ export function buildConnectionTokenResolver(
712
725
  destinationUrl: string,
713
726
  inputCredentialTarget: "mcp" | "http_api",
714
727
  connectionUseAttribution?: ConnectionUseAttribution,
728
+ recordUsage = true,
715
729
  ): Promise<ResolveConnectionCredentialResult> => {
716
730
  if (cred.status !== "active") {
717
731
  return authNeededForStatus(cred, ref);
@@ -753,7 +767,9 @@ export function buildConnectionTokenResolver(
753
767
  : {}),
754
768
  };
755
769
  }
756
- await deps.recordUsed(db, cred.workspaceId, cred.id, cred.subjectId);
770
+ if (recordUsage) {
771
+ await deps.recordUsed(db, cred.workspaceId, cred.id, cred.subjectId);
772
+ }
757
773
  return {
758
774
  status: "ok",
759
775
  headers: material.headers,
@@ -830,11 +846,15 @@ export function buildConnectionTokenResolver(
830
846
  };
831
847
 
832
848
  return async (input) => {
849
+ const credentialResolutionMode = input.credentialResolutionMode ?? "execution";
833
850
  let ref = input.connectionRef;
834
851
  let subjectId = input.subjectId;
835
852
  let credentialWorkspaceId = input.workspaceId;
836
- let expectedAuthorityGeneration: number | undefined;
853
+ let expectedAuthorityGeneration = input.expectedAuthorityGeneration;
837
854
  let connectionUseAttribution: ConnectionUseAttribution | undefined;
855
+ if (ref.authoritySource === "host") {
856
+ return authNeeded(ref, "unsupported_auth", ref.connectionId);
857
+ }
838
858
  // Repository-scoped provider bindings require a broker that can prove the
839
859
  // selected-resource boundary. The generic standalone credential store has
840
860
  // no provider-specific containment adapter, so it must fail closed instead
@@ -870,6 +890,16 @@ export function buildConnectionTokenResolver(
870
890
  ref.connectionId,
871
891
  );
872
892
  }
893
+ if (
894
+ expectedAuthorityGeneration !== undefined &&
895
+ authorization.attribution.connectionGeneration !== expectedAuthorityGeneration
896
+ ) {
897
+ return authNeeded(
898
+ ref,
899
+ authorityReasonForScope(ref.subjectScope === "subject"),
900
+ ref.connectionId,
901
+ );
902
+ }
873
903
  connectionUseAttribution = authorization.attribution;
874
904
  expectedAuthorityGeneration = authorization.attribution.connectionGeneration;
875
905
  const expectedPersonal = authorization.attribution.scope !== "workspace";
@@ -906,6 +936,12 @@ export function buildConnectionTokenResolver(
906
936
  authority.connectionId,
907
937
  );
908
938
  }
939
+ if (
940
+ expectedAuthorityGeneration !== undefined &&
941
+ authority.connectionGeneration !== expectedAuthorityGeneration
942
+ ) {
943
+ return authNeeded(ref, authorityReasonForScope(expectedPersonal), authority.connectionId);
944
+ }
909
945
  connectionUseAttribution = authorization.attribution;
910
946
  expectedAuthorityGeneration = authority.connectionGeneration;
911
947
  // Personal resources are organization-user owned and may originate in a
@@ -950,7 +986,19 @@ export function buildConnectionTokenResolver(
950
986
  if (!connectionBindingMatches(cred, ref, input.destinationUrl)) {
951
987
  return authNeeded(ref, "missing_connection", cred.id);
952
988
  }
953
- if (shouldRefresh(cred, input.forceRefresh === true, deps.now())) {
989
+ const now = deps.now();
990
+ if (
991
+ credentialResolutionMode === "preflight" &&
992
+ cred.kind === "oauth2" &&
993
+ (input.forceRefresh === true ||
994
+ (cred.expiresAt !== null && cred.expiresAt.getTime() <= now.getTime()))
995
+ ) {
996
+ return authNeeded(ref, "refresh_failed", cred.id);
997
+ }
998
+ if (
999
+ credentialResolutionMode === "execution" &&
1000
+ shouldRefresh(cred, input.forceRefresh === true, now)
1001
+ ) {
954
1002
  try {
955
1003
  cred = await refreshSingleFlight(cred, ref);
956
1004
  } catch (error) {
@@ -998,6 +1046,7 @@ export function buildConnectionTokenResolver(
998
1046
  input.destinationUrl,
999
1047
  input.credentialTarget ?? "mcp",
1000
1048
  connectionUseAttribution,
1049
+ credentialResolutionMode === "execution",
1001
1050
  );
1002
1051
  };
1003
1052
  }