@opengeni/db 0.27.8 → 0.27.11

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 (63) hide show
  1. package/dist/{chunk-M6EOXYHK.js → chunk-JYQPJ6Y5.js} +59 -50
  2. package/dist/chunk-JYQPJ6Y5.js.map +1 -0
  3. package/dist/{chunk-IHPCI4GV.js → chunk-RPHPNVWW.js} +3 -1
  4. package/dist/chunk-RPHPNVWW.js.map +1 -0
  5. package/dist/codex-token-resolver.d.ts +50 -8
  6. package/dist/connection-token-resolver.d.ts +45 -7
  7. package/dist/database.d.ts +137 -0
  8. package/dist/index.d.ts +83 -184
  9. package/dist/index.js +2983 -2691
  10. package/dist/index.js.map +1 -1
  11. package/dist/insights.d.ts +1 -1
  12. package/dist/memory-governance.d.ts +1 -1
  13. package/dist/new-session-drafts.d.ts +1 -1
  14. package/dist/preference-registry.d.ts +1 -1
  15. package/dist/provision-roles.d.ts +1 -1
  16. package/dist/provision-roles.js +1 -1
  17. package/dist/runtime-posture.d.ts +3 -3
  18. package/dist/schema.d.ts +155 -96
  19. package/dist/schema.js +3 -1
  20. package/dist/scoped-knowledge.d.ts +1 -1
  21. package/dist/session-control.d.ts +1 -1
  22. package/dist/session-queue-commands.d.ts +1 -1
  23. package/dist/session-realtime-context.d.ts +1 -1
  24. package/dist/session-realtime-ledger.d.ts +1 -1
  25. package/dist/session-realtime-mirror.d.ts +1 -1
  26. package/dist/session-realtime-state.d.ts +1 -1
  27. package/dist/session-realtime-terminal.d.ts +1 -1
  28. package/dist/session-realtime.d.ts +1 -1
  29. package/dist/session-tool-call-settlement.d.ts +1 -1
  30. package/dist/turn-initiator.d.ts +1 -1
  31. package/dist/workspace-artifacts.d.ts +1 -1
  32. package/dist/workspace-instruction-policies.d.ts +1 -1
  33. package/drizzle/0171_social_connection_subject_ownership.sql +53 -0
  34. package/drizzle/0172_retire_model_visible_github_token.sql +86 -0
  35. package/drizzle/0173_codex_auth_boundaries.sql +107 -0
  36. package/drizzle/0174_session_wake_live_interruption.sql +80 -0
  37. package/package.json +4 -4
  38. package/src/codex-token-resolver.ts +102 -49
  39. package/src/connection-token-resolver.ts +67 -26
  40. package/src/database.ts +393 -0
  41. package/src/index.ts +747 -645
  42. package/src/insights.ts +2 -2
  43. package/src/memory-governance.ts +2 -2
  44. package/src/new-session-drafts.ts +1 -1
  45. package/src/preference-registry.ts +2 -2
  46. package/src/provision-roles.ts +1 -1
  47. package/src/runtime-posture.ts +3 -1
  48. package/src/schema.ts +70 -50
  49. package/src/scoped-knowledge.ts +2 -2
  50. package/src/session-control.ts +1 -1
  51. package/src/session-queue-commands.ts +1 -1
  52. package/src/session-realtime-context.ts +1 -1
  53. package/src/session-realtime-ledger.ts +1 -1
  54. package/src/session-realtime-mirror.ts +1 -1
  55. package/src/session-realtime-state.ts +1 -1
  56. package/src/session-realtime-terminal.ts +1 -1
  57. package/src/session-realtime.ts +1 -1
  58. package/src/session-tool-call-settlement.ts +1 -1
  59. package/src/turn-initiator.ts +1 -1
  60. package/src/workspace-artifacts.ts +2 -2
  61. package/src/workspace-instruction-policies.ts +2 -2
  62. package/dist/chunk-IHPCI4GV.js.map +0 -1
  63. package/dist/chunk-M6EOXYHK.js.map +0 -1
package/src/index.ts CHANGED
@@ -154,7 +154,12 @@ import {
154
154
  VERCEL_AI_GATEWAY_CONNECTION_ROLE,
155
155
  type Settings,
156
156
  } from "@opengeni/config";
157
- import { boundModelToolOutputItem, isCodexBilledModel } from "@opengeni/codex";
157
+ import {
158
+ boundModelToolOutputItem,
159
+ isCodexBilledModel,
160
+ refreshCodexToken,
161
+ type CodexFetch,
162
+ } from "@opengeni/codex";
158
163
  // Re-exported so consumers get the whole codex-billed detection surface (the pure
159
164
  // prefix test + the credential-aware predicates below) from a single import.
160
165
  export { isCodexBilledModel } from "@opengeni/codex";
@@ -187,16 +192,10 @@ import {
187
192
  type FrozenTurnInitiator,
188
193
  } from "./turn-initiator";
189
194
  export { frozenInitiatorForCommandActor, type FrozenTurnInitiator } from "./turn-initiator";
190
- import type { PgDatabase, PgTransactionConfig } from "drizzle-orm/pg-core";
191
- import { drizzle } from "drizzle-orm/postgres-js";
192
- import postgres from "postgres";
193
- import { decryptEnvironmentValue } from "./environment-crypto";
195
+ import { decryptEnvironmentValue, encryptEnvironmentValue } from "./environment-crypto";
194
196
  import { sanitizeEventPayload, sanitizeModelPayload } from "./event-payload-sanitizer";
195
197
  import { seedNewSessionDraftInTransaction } from "./new-session-drafts";
196
- import {
197
- runIdempotentPersistenceTransaction,
198
- type IdempotentPersistenceTransactionOptions,
199
- } from "./persistence-errors";
198
+ import { runIdempotentPersistenceTransaction } from "./persistence-errors";
200
199
  import {
201
200
  closePendingSessionToolCallsInTransaction,
202
201
  historyCallId,
@@ -303,22 +302,58 @@ export {
303
302
  // Workspace Memory V1 pure domain surface (gates, render, canonical prompt text).
304
303
  export * from "./memory-domain";
305
304
 
306
- // §7.7 driver widening (Step I). `Database` is the structural, cross-driver
307
- // query-layer port: every helper in this file accepts `db: Database` and uses
308
- // only the methods present on drizzle's base `PgDatabase` (select/insert/update/
309
- // delete/transaction/execute). Widening from the concrete
310
- // `PostgresJsDatabase<typeof schema>` to `PgDatabase<any, typeof schema>` is a
311
- // pure TYPE change — no runtime behavior changes — that lets an embedded host
312
- // inject ANY drizzle pg driver handle (node-postgres, neon-http, etc.) bound to
313
- // OpenGeni's schema, not just the postgres-js handle `createDb` builds. The
314
- // `any` for the query-result HKT is deliberate: it keeps `db.execute(sql\`…\`)`
315
- // callable across drivers whose raw-result shapes differ (postgres-js returns a
316
- // row array; node-postgres returns `{ rows }`). The three raw `db.execute(…)`
317
- // reads that index a row array (`getManagedUserByEmail` here is the only
318
- // host-facing one — see `userLookup`) stay postgres-js-shaped for standalone;
319
- // `userLookup` is the injection seam for hosts on a different driver.
320
- // `PostgresJsDatabase<typeof schema>` is assignable to this, so standalone is
321
- // unaffected.
305
+ import {
306
+ dbBindingFor,
307
+ rawRows,
308
+ retryRlsPersistence,
309
+ retryWorkspacePersistence,
310
+ rlsContextForWorkspace,
311
+ setRlsContext,
312
+ setSubjectRlsContext,
313
+ withAccountRls,
314
+ withRlsContext,
315
+ withWorkspaceRls,
316
+ withWorkspaceSubjectRls,
317
+ withWorkspaceUsageLock,
318
+ type Database,
319
+ } from "./database";
320
+ export {
321
+ createDb,
322
+ registerDbBinding,
323
+ rlsContextForWorkspace,
324
+ rlsStrategyFor,
325
+ setRlsContext,
326
+ setSubjectRlsContext,
327
+ withAccountRls,
328
+ withDatabaseStatementTimeout,
329
+ withRlsContext,
330
+ withWorkspaceRls,
331
+ withWorkspaceSubjectRls,
332
+ withWorkspaceUsageLock,
333
+ type CreateDbOptions,
334
+ type Database,
335
+ type DbClient,
336
+ type RlsContext,
337
+ type RlsStrategy,
338
+ type UserLookup,
339
+ } from "./database";
340
+ import {
341
+ buildCodexTokenResolver as buildCodexTokenResolverCore,
342
+ fetchCodexRateLimitResetCreditsForAccount as fetchCodexRateLimitResetCreditsForAccountCore,
343
+ fetchCodexUsageForAccount as fetchCodexUsageForAccountCore,
344
+ type CodexAccountUsageSnapshot,
345
+ type CodexAuthDeps,
346
+ type CodexCredentialForRun,
347
+ type CodexCredentialTokens,
348
+ } from "./codex-token-resolver";
349
+ import {
350
+ buildConnectionTokenResolver as buildConnectionTokenResolverCore,
351
+ refreshOAuthConnectionCredential as refreshOAuthConnectionCredentialCore,
352
+ type ConnectionBrokerDeps,
353
+ type ConnectionCredentialForBroker,
354
+ type ConnectionTokenResolverOptions,
355
+ } from "./connection-token-resolver";
356
+
322
357
  function parsedPersonalConnectionDelegations(
323
358
  value: unknown,
324
359
  context: string,
@@ -330,8 +365,6 @@ function parsedPersonalConnectionDelegations(
330
365
  return parsed.data.map((delegation) => ({ ...delegation }));
331
366
  }
332
367
 
333
- export type Database = PgDatabase<any, typeof schema>;
334
-
335
368
  /** Raised when a durable session tool-policy write lost its version fence. */
336
369
  export class SessionToolPolicyVersionConflictError extends Error {
337
370
  readonly code = "SESSION_TOOL_POLICY_CONFLICT";
@@ -342,16 +375,6 @@ export class SessionToolPolicyVersionConflictError extends Error {
342
375
  }
343
376
  }
344
377
 
345
- export type DbClient = {
346
- db: Database;
347
- close: () => Promise<void>;
348
- };
349
-
350
- export type RlsContext = {
351
- accountId: string;
352
- workspaceId?: string | null;
353
- };
354
-
355
378
  export type NestedAgentDepthPolicySource = "session" | "workspace" | "deployment" | "default";
356
379
 
357
380
  export type SessionDepthPolicy = {
@@ -414,165 +437,6 @@ export class SessionSpawnDeniedDbError extends Error {
414
437
  }
415
438
  }
416
439
 
417
- /**
418
- * RLS posture for the connection OpenGeni's query layer runs over (Step I, §7.7).
419
- *
420
- * - `"force"` (DEFAULT — today's standalone behavior, byte-for-byte): OpenGeni
421
- * connects as a NON-OWNER role (`opengeni_app`) and every table carries
422
- * `FORCE ROW LEVEL SECURITY`, so the workspace/account GUCs set by
423
- * `setRlsContext` are the ONLY thing that admits rows — even the table owner
424
- * is subject to RLS. This is the Fork-A isolation guarantee.
425
- * - `"scoped"` (embedded Fork-B opt-in): the host runs OpenGeni's queries over a
426
- * role that OWNS the dedicated schema (RLS need not be forced for that role),
427
- * relying on the host's own tenant boundary. OpenGeni STILL emits the
428
- * `set_config('opengeni.account_id'/'workspace_id', …)` GUCs defensively on
429
- * every scoped query, so the application query path is byte-identical between
430
- * the two strategies and the app code is RLS-mode-agnostic. The strategy is a
431
- * declared posture (consumed by `provisionRoles` and as a documented
432
- * invariant), NOT a query-path branch — there is deliberately no `if
433
- * (strategy === …)` anywhere in the helpers below. Picking `"scoped"` does not
434
- * relax any GUC; it only changes which DB role the host provisions/connects as
435
- * and asserts that the host accepts owning the isolation boundary.
436
- */
437
- export type RlsStrategy = "force" | "scoped";
438
-
439
- /**
440
- * Resolve a host-IdP/Better-Auth user *identifier* by email. Injected via
441
- * `createDb({ userLookup })` (Step I). UNSET → today's raw parameterized select
442
- * against Better Auth's `auth_users` table (see `getManagedUserByEmail`), which
443
- * relies on the postgres-js array-shaped `db.execute` result. An embedded host
444
- * whose identity lives elsewhere (a different IdP table, a different driver, or
445
- * a non-`auth_users` user store) injects this closure so OpenGeni never touches
446
- * `auth_users` directly. Returns the user id, or null when no such user exists.
447
- */
448
- export type UserLookup = (db: Database, email: string) => Promise<string | null>;
449
-
450
- export type CreateDbOptions = {
451
- /**
452
- * The Postgres `search_path` for this connection (Step I, §7.8 runtime half).
453
- * UNSET → today's behavior: NO `search_path` startup parameter is sent, so the
454
- * server default applies (`public` for standalone, where every table + the
455
- * `vector` extension + `gen_random_uuid()` live). For an embedded dedicated
456
- * schema, pass e.g. `"opengeni,opengeni_private,public"` — postgres-js sends
457
- * it as a per-session startup parameter (the supported, query-param-free way;
458
- * URL `?search_path=` is IGNORED by postgres-js). Keep `public` LAST so the
459
- * `vector` type and `gen_random_uuid()` (which live in `public` on the
460
- * pgvector image) still resolve — the schema-isolation contract live footgun.
461
- */
462
- searchPath?: string;
463
- /** RLS posture; defaults to `"force"` (today's standalone). */
464
- rlsStrategy?: RlsStrategy;
465
- /** Host-provided user-by-email resolver; unset → today's raw `auth_users` query. */
466
- userLookup?: UserLookup;
467
- /** postgres-js pool size; defaults to today's `10`. */
468
- max?: number;
469
- /**
470
- * Connection-local default transaction isolation sent in the postgres-js
471
- * startup parameters. This is intentionally not a role/database default:
472
- * tests and embedded callers can exercise a different ambient isolation
473
- * without mutating a shared PostgreSQL role or affecting other connections.
474
- */
475
- isolationLevel?: postgres.ConnectionParameters["default_transaction_isolation"];
476
- };
477
-
478
- /**
479
- * The active RLS strategy + userLookup for an injected `Database`, recorded in a
480
- * side WeakMap so helpers (and `getManagedUserByEmail`) can consult the host's
481
- * binding without changing every call signature. A handle with no recorded
482
- * config (e.g. one built outside `createDb`, or in a test) falls back to the
483
- * standalone defaults: `rlsStrategy: "force"`, raw `auth_users` lookup.
484
- */
485
- type DbBinding = { rlsStrategy: RlsStrategy; userLookup?: UserLookup };
486
- const dbBindings = new WeakMap<object, DbBinding>();
487
-
488
- /** The strategy bound to a handle (or the `"force"` default). */
489
- export function rlsStrategyFor(db: Database): RlsStrategy {
490
- return dbBindings.get(db as unknown as object)?.rlsStrategy ?? "force";
491
- }
492
-
493
- /**
494
- * Run a raw SQL query and read its rows as a typed array.
495
- *
496
- * Why this exists: the Step I driver widening (`Database = PgDatabase<any, …>`)
497
- * deliberately sets the query-result HKT to `any` so `db.execute(…)` is callable
498
- * across drivers whose raw-result shapes differ (postgres-js → row array;
499
- * node-postgres → `{ rows }`). A side effect is that `db.execute<T>(…)` now
500
- * resolves to `any`, erasing the per-row element type at the call site. OpenGeni's
501
- * OWN internal raw queries usually run over the postgres-js handle `createDb`
502
- * builds (array result), while an embedded host may inject a node-postgres style
503
- * driver (`{ rows }`). Normalize those two standard shapes in one place; reject
504
- * an unknown driver result rather than silently treating it as an empty query.
505
- */
506
- async function rawRows<T extends Record<string, unknown>>(
507
- executor: Pick<Database, "execute">,
508
- query: SQL,
509
- ): Promise<T[]> {
510
- const result = await executor.execute<T>(query);
511
- if (Array.isArray(result)) {
512
- return result as unknown as T[];
513
- }
514
- const rows = (result as unknown as { rows?: unknown }).rows;
515
- if (Array.isArray(rows)) {
516
- return rows as T[];
517
- }
518
- throw new Error("Unsupported database execute() result shape");
519
- }
520
-
521
- export function createDb(databaseUrl: string, options: CreateDbOptions = {}): DbClient {
522
- // `prepare: false` is REQUIRED for Azure Database for PostgreSQL Flexible
523
- // Server's transaction-pooling PgBouncer: postgres-js's default named prepared
524
- // statements (`s_N`) are bound to one backend, but a transaction pooler hands
525
- // each transaction a different backend, so a later `execute` intermittently
526
- // throws `prepared statement "s_N" does not exist`. Every RLS read in this
527
- // module (set_config + SELECT inside one db.transaction) rides on this pool, so
528
- // the failure surfaces as a "worked, then didn't" credential/permission read.
529
- // idle_timeout + max_lifetime recycle connections so a pooler-recycled backend
530
- // is never reused indefinitely; application_name aids server-side diagnostics.
531
- const client = postgres(databaseUrl, {
532
- max: options.max ?? 10,
533
- prepare: false,
534
- idle_timeout: 30,
535
- max_lifetime: 1800,
536
- // `connection` carries per-session Postgres STARTUP parameters. `application_name`
537
- // (always) aids server-side diagnostics; `search_path` (embedded only) is the
538
- // supported, query-param-free way to scope a connection to a dedicated schema —
539
- // postgres-js IGNORES a URL `?search_path=`. Unset searchPath → omit it so the
540
- // server default (`public`) is unchanged for standalone.
541
- connection: {
542
- application_name: "opengeni",
543
- ...(options.searchPath ? { search_path: options.searchPath } : {}),
544
- ...(options.isolationLevel ? { default_transaction_isolation: options.isolationLevel } : {}),
545
- },
546
- });
547
- const db = drizzle(client, { schema });
548
- dbBindings.set(db as unknown as object, {
549
- rlsStrategy: options.rlsStrategy ?? "force",
550
- ...(options.userLookup ? { userLookup: options.userLookup } : {}),
551
- });
552
- return {
553
- db,
554
- close: async () => {
555
- await client.end();
556
- },
557
- };
558
- }
559
-
560
- /**
561
- * Register a host's `rlsStrategy`/`userLookup` against an externally-constructed
562
- * `Database` handle (e.g. one the embedded host built from its own driver and
563
- * injected, rather than via `createDb`). Lets the same WeakMap-backed lookups
564
- * work for injected handles. Standalone never calls this (it uses `createDb`).
565
- */
566
- export function registerDbBinding(
567
- db: Database,
568
- binding: { rlsStrategy?: RlsStrategy; userLookup?: UserLookup },
569
- ): void {
570
- dbBindings.set(db as unknown as object, {
571
- rlsStrategy: binding.rlsStrategy ?? "force",
572
- ...(binding.userLookup ? { userLookup: binding.userLookup } : {}),
573
- });
574
- }
575
-
576
440
  // ---------------------------------------------------------------------------
577
441
  // Durable host export
578
442
  // ---------------------------------------------------------------------------
@@ -1048,187 +912,6 @@ export async function getHostExportConsumerStatus(
1048
912
  };
1049
913
  }
1050
914
 
1051
- export async function setRlsContext(db: Database, context: RlsContext): Promise<void> {
1052
- // Fail loud on an empty/blank account id: a "" account would set an RLS GUC
1053
- // that matches no tenant row, silently returning zero rows from every scoped
1054
- // read (a phantom "not found" / "no active subscription"). An RLS context with
1055
- // no account is always a bug at the call site, never a valid query scope.
1056
- if (typeof context.accountId !== "string" || context.accountId.trim() === "") {
1057
- throw new Error("setRlsContext: a non-empty accountId is required to establish an RLS context");
1058
- }
1059
- await db.execute(sql`select set_config('opengeni.account_id', ${context.accountId}, true)`);
1060
- await db.execute(
1061
- sql`select set_config('opengeni.workspace_id', ${context.workspaceId ?? ""}, true)`,
1062
- );
1063
- await db.execute(sql`select set_config('opengeni.sandbox_recovery_protocol_v2', '1', true)`);
1064
- }
1065
-
1066
- export async function withRlsContext<T>(
1067
- db: Database,
1068
- context: RlsContext,
1069
- fn: (db: Database) => Promise<T>,
1070
- transactionConfig?: PgTransactionConfig,
1071
- ): Promise<T> {
1072
- return await db.transaction(async (tx) => {
1073
- const scoped = tx as unknown as Database;
1074
- await setRlsContext(scoped, context);
1075
- // Defense-in-depth: read the LOCAL GUC back on THIS backend BEFORE running
1076
- // the scoped query. The set_config and this read share one db.transaction,
1077
- // which a transaction pooler pins to a single backend — so a mismatch here
1078
- // means the context was genuinely lost (a torn transaction / pooler backend
1079
- // swap), not normal operation. Without this guard such an event runs the
1080
- // scoped read with an empty account_id and returns zero RLS-visible rows,
1081
- // manufacturing a phantom "no active subscription" from a credential that is
1082
- // in fact active. Convert that silent false into a loud, root-cause-bearing
1083
- // error so the caller can retry rather than permanently mis-decide.
1084
- const applied = await tx.execute<{
1085
- account_id: string | null;
1086
- workspace_id: string | null;
1087
- }>(
1088
- sql`select
1089
- current_setting('opengeni.account_id', true) as account_id,
1090
- current_setting('opengeni.workspace_id', true) as workspace_id`,
1091
- );
1092
- const appliedAccountId = applied[0]?.account_id ?? "";
1093
- const expectedWorkspaceId = context.workspaceId ?? "";
1094
- const appliedWorkspaceId = applied[0]?.workspace_id ?? "";
1095
- if (appliedAccountId !== context.accountId) {
1096
- throw new Error(
1097
- `RLS context not applied on the active backend: expected account ${context.accountId}, got "${appliedAccountId}"`,
1098
- );
1099
- }
1100
- if (appliedWorkspaceId !== expectedWorkspaceId) {
1101
- throw new Error(
1102
- `RLS context not applied on the active backend: expected workspace "${expectedWorkspaceId}", got "${appliedWorkspaceId}"`,
1103
- );
1104
- }
1105
- return await fn(scoped);
1106
- }, transactionConfig);
1107
- }
1108
-
1109
- /**
1110
- * Run one bounded database operation on a transaction-pinned backend.
1111
- *
1112
- * Callers that also have an application deadline should check their abort
1113
- * signal before returning from `fn`; throwing there rolls the transaction back
1114
- * even when the application deadline won a surrounding Promise race.
1115
- */
1116
- export async function withDatabaseStatementTimeout<T>(
1117
- db: Database,
1118
- timeoutMs: number,
1119
- fn: (db: Database) => Promise<T>,
1120
- ): Promise<T> {
1121
- if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
1122
- throw new Error("withDatabaseStatementTimeout requires a positive timeout");
1123
- }
1124
- const boundedTimeoutMs = Math.max(1, Math.floor(timeoutMs));
1125
- return await db.transaction(async (tx) => {
1126
- const scoped = tx as unknown as Database;
1127
- await scoped.execute(
1128
- sql`select set_config('statement_timeout', ${`${boundedTimeoutMs}ms`}, true)`,
1129
- );
1130
- return await fn(scoped);
1131
- });
1132
- }
1133
-
1134
- export async function rlsContextForWorkspace(
1135
- db: Database,
1136
- workspaceId: string,
1137
- ): Promise<RlsContext> {
1138
- const [row] = await db
1139
- .select({ accountId: schema.workspaces.accountId })
1140
- .from(schema.workspaces)
1141
- .where(eq(schema.workspaces.id, workspaceId))
1142
- .limit(1);
1143
- if (!row) {
1144
- throw new Error(`Workspace not found: ${workspaceId}`);
1145
- }
1146
- return { accountId: row.accountId, workspaceId };
1147
- }
1148
-
1149
- export async function withWorkspaceRls<T>(
1150
- db: Database,
1151
- workspaceId: string,
1152
- fn: (db: Database) => Promise<T>,
1153
- ): Promise<T> {
1154
- return await withRlsContext(db, await rlsContextForWorkspace(db, workspaceId), fn);
1155
- }
1156
-
1157
- async function retryWorkspacePersistence<T>(
1158
- db: Database,
1159
- workspaceId: string,
1160
- options: IdempotentPersistenceTransactionOptions,
1161
- fn: (db: Database) => Promise<T>,
1162
- ): Promise<T> {
1163
- return await runIdempotentPersistenceTransaction(options, async () => {
1164
- return await withWorkspaceRls(db, workspaceId, fn);
1165
- });
1166
- }
1167
-
1168
- async function retryRlsPersistence<T>(
1169
- db: Database,
1170
- context: RlsContext,
1171
- options: IdempotentPersistenceTransactionOptions,
1172
- fn: (db: Database) => Promise<T>,
1173
- ): Promise<T> {
1174
- return await runIdempotentPersistenceTransaction(options, async () => {
1175
- return await withRlsContext(db, context, fn);
1176
- });
1177
- }
1178
-
1179
- /**
1180
- * Personal workspace data needs both tenant and authenticated-principal GUCs.
1181
- * `session_pins` uses this helper so FORCE RLS rejects another member's rows
1182
- * even if a future query accidentally omits its explicit subject predicate.
1183
- */
1184
- export async function withWorkspaceSubjectRls<T>(
1185
- db: Database,
1186
- workspaceId: string,
1187
- subjectId: string,
1188
- fn: (db: Database) => Promise<T>,
1189
- transactionConfig?: PgTransactionConfig,
1190
- ): Promise<T> {
1191
- if (!subjectId.trim()) {
1192
- throw new Error("withWorkspaceSubjectRls: a non-empty subjectId is required");
1193
- }
1194
- const context = await rlsContextForWorkspace(db, workspaceId);
1195
- return await withRlsContext(
1196
- db,
1197
- context,
1198
- async (scopedDb) => {
1199
- await setSubjectRlsContext(scopedDb, subjectId);
1200
- return await fn(scopedDb);
1201
- },
1202
- transactionConfig,
1203
- );
1204
- }
1205
-
1206
- /** Apply and verify actor-private RLS on an already transaction-pinned handle. */
1207
- export async function setSubjectRlsContext(db: Database, subjectId: string): Promise<void> {
1208
- if (!subjectId.trim()) {
1209
- throw new Error("setSubjectRlsContext: a non-empty subjectId is required");
1210
- }
1211
- await db.execute(sql`select set_config('opengeni.subject_id', ${subjectId}, true)`);
1212
- const applied = await db.execute<{ subject_id: string | null }>(
1213
- sql`select current_setting('opengeni.subject_id', true) as subject_id`,
1214
- );
1215
- if ((applied[0]?.subject_id ?? "") !== subjectId) {
1216
- throw new Error("Authenticated subject RLS context was not applied on the active backend");
1217
- }
1218
- }
1219
-
1220
- export async function withWorkspaceUsageLock<T>(
1221
- db: Database,
1222
- workspaceId: string,
1223
- fn: (db: Database) => Promise<T>,
1224
- ): Promise<T> {
1225
- const context = await rlsContextForWorkspace(db, workspaceId);
1226
- return await withRlsContext(db, context, async (scopedDb) => {
1227
- await scopedDb.execute(sql`select pg_advisory_xact_lock(hashtext(${`usage:${workspaceId}`}))`);
1228
- return await fn(scopedDb);
1229
- });
1230
- }
1231
-
1232
915
  export type DocumentIndexAuthority = {
1233
916
  authorityKind: DocumentAuthorityKind;
1234
917
  authorityWorkspaceId: string | null;
@@ -1278,14 +961,6 @@ export async function resolveDocumentIndexAuthority(
1278
961
  };
1279
962
  }
1280
963
 
1281
- export async function withAccountRls<T>(
1282
- db: Database,
1283
- accountId: string,
1284
- fn: (db: Database) => Promise<T>,
1285
- ): Promise<T> {
1286
- return await withRlsContext(db, { accountId, workspaceId: null }, fn);
1287
- }
1288
-
1289
964
  export const allWorkspacePermissions: Permission[] = [
1290
965
  "workspace:read",
1291
966
  "workspace:admin",
@@ -2096,7 +1771,7 @@ export async function removeWorkspaceMember(
2096
1771
  * unknown users are deferred.
2097
1772
  */
2098
1773
  export async function getManagedUserByEmail(db: Database, email: string): Promise<string | null> {
2099
- const binding = dbBindings.get(db as unknown as object);
1774
+ const binding = dbBindingFor(db);
2100
1775
  if (binding?.userLookup) {
2101
1776
  return await binding.userLookup(db, email);
2102
1777
  }
@@ -3499,6 +3174,7 @@ export type WorkspaceStateMemoryRecord = {
3499
3174
  export type CreateSocialConnectionInput = {
3500
3175
  accountId: string;
3501
3176
  workspaceId: string;
3177
+ subjectId?: string | null;
3502
3178
  provider: SocialProvider;
3503
3179
  accountHandle: string;
3504
3180
  accountName?: string | null;
@@ -3514,6 +3190,7 @@ export type CreateSocialConnectionInput = {
3514
3190
  export type UpsertSocialOAuthConnectionInput = {
3515
3191
  accountId: string;
3516
3192
  workspaceId: string;
3193
+ subjectId?: string | null;
3517
3194
  provider: SocialProvider;
3518
3195
  accountHandle: string;
3519
3196
  accountName?: string | null;
@@ -3526,6 +3203,7 @@ export type UpsertSocialOAuthConnectionInput = {
3526
3203
  export type UpdateSocialConnectionCredentialInput = {
3527
3204
  workspaceId: string;
3528
3205
  connectionId: string;
3206
+ subjectId?: string | null;
3529
3207
  credentialEncrypted?: string | null;
3530
3208
  status?: SocialConnectionStatus;
3531
3209
  tokenMetadata?: Record<string, unknown>;
@@ -3534,6 +3212,7 @@ export type UpdateSocialConnectionCredentialInput = {
3534
3212
  export type CreateSocialPostInput = {
3535
3213
  accountId: string;
3536
3214
  workspaceId: string;
3215
+ subjectId?: string | null;
3537
3216
  connectionId: string;
3538
3217
  externalPostId?: string | null;
3539
3218
  url?: string | null;
@@ -3672,22 +3351,6 @@ export type SlackBotDeleteOperation = {
3672
3351
  updatedAt: Date;
3673
3352
  };
3674
3353
 
3675
- export type ConnectionCredentialForBroker = {
3676
- id: string;
3677
- accountId: string;
3678
- workspaceId: string;
3679
- subjectId: string | null;
3680
- providerDomain: string;
3681
- kind: ConnectionKind;
3682
- status: ConnectionStatus;
3683
- credential: Record<string, unknown>;
3684
- grantedScopes: string[];
3685
- expiresAt: Date | null;
3686
- lastRefreshAt: Date | null;
3687
- version: number;
3688
- metadata: Record<string, unknown>;
3689
- };
3690
-
3691
3354
  export type IntegrationOAuthClientForUse = {
3692
3355
  id: string;
3693
3356
  issuer: string;
@@ -9262,11 +8925,13 @@ export async function createSocialConnection(
9262
8925
  db,
9263
8926
  { accountId: input.accountId, workspaceId: input.workspaceId },
9264
8927
  async (scopedDb) => {
8928
+ if (input.subjectId) await setSubjectRlsContext(scopedDb, input.subjectId);
9265
8929
  const [row] = await scopedDb
9266
8930
  .insert(schema.socialConnections)
9267
8931
  .values({
9268
8932
  accountId: input.accountId,
9269
8933
  workspaceId: input.workspaceId,
8934
+ subjectId: input.subjectId ?? null,
9270
8935
  provider: input.provider,
9271
8936
  accountHandle: input.accountHandle,
9272
8937
  accountName: input.accountName ?? null,
@@ -9300,11 +8965,13 @@ export async function upsertSocialOAuthConnection(
9300
8965
  db,
9301
8966
  { accountId: input.accountId, workspaceId: input.workspaceId },
9302
8967
  async (scopedDb) => {
8968
+ if (input.subjectId) await setSubjectRlsContext(scopedDb, input.subjectId);
9303
8969
  const [row] = await scopedDb
9304
8970
  .insert(schema.socialConnections)
9305
8971
  .values({
9306
8972
  accountId: input.accountId,
9307
8973
  workspaceId: input.workspaceId,
8974
+ subjectId: input.subjectId ?? null,
9308
8975
  provider: input.provider,
9309
8976
  accountHandle: input.accountHandle,
9310
8977
  accountName: input.accountName ?? null,
@@ -9315,12 +8982,21 @@ export async function upsertSocialOAuthConnection(
9315
8982
  tokenMetadata: input.tokenMetadata ?? {},
9316
8983
  })
9317
8984
  .onConflictDoUpdate({
9318
- target: [
9319
- schema.socialConnections.workspaceId,
9320
- schema.socialConnections.provider,
9321
- schema.socialConnections.accountHandle,
9322
- ],
8985
+ target: input.subjectId
8986
+ ? [
8987
+ schema.socialConnections.workspaceId,
8988
+ schema.socialConnections.subjectId,
8989
+ schema.socialConnections.provider,
8990
+ ]
8991
+ : [
8992
+ schema.socialConnections.workspaceId,
8993
+ schema.socialConnections.provider,
8994
+ schema.socialConnections.accountHandle,
8995
+ ],
8996
+ targetWhere: input.subjectId ? sql`subject_id is not null` : sql`subject_id is null`,
9323
8997
  set: {
8998
+ accountId: input.accountId,
8999
+ accountHandle: input.accountHandle,
9324
9000
  accountName: input.accountName ?? null,
9325
9001
  externalAccountId: input.externalAccountId ?? null,
9326
9002
  status: "connected",
@@ -9348,8 +9024,9 @@ export async function loadSocialConnectionCredential(
9348
9024
  db: Database,
9349
9025
  workspaceId: string,
9350
9026
  connectionId: string,
9027
+ subjectId?: string | null,
9351
9028
  ): Promise<{ connection: SocialConnection; credentialEncrypted: string | null } | null> {
9352
- return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
9029
+ return await withSocialConnectionSubjectRls(db, workspaceId, subjectId, async (scopedDb) => {
9353
9030
  const [row] = await scopedDb
9354
9031
  .select()
9355
9032
  .from(schema.socialConnections)
@@ -9357,6 +9034,7 @@ export async function loadSocialConnectionCredential(
9357
9034
  and(
9358
9035
  eq(schema.socialConnections.workspaceId, workspaceId),
9359
9036
  eq(schema.socialConnections.id, connectionId),
9037
+ socialConnectionSubjectVisibility(subjectId),
9360
9038
  ),
9361
9039
  )
9362
9040
  .limit(1);
@@ -9371,26 +9049,32 @@ export async function updateSocialConnectionCredential(
9371
9049
  db: Database,
9372
9050
  input: UpdateSocialConnectionCredentialInput,
9373
9051
  ): Promise<SocialConnection | null> {
9374
- return await withWorkspaceRls(db, input.workspaceId, async (scopedDb) => {
9375
- const [row] = await scopedDb
9376
- .update(schema.socialConnections)
9377
- .set({
9378
- ...(input.credentialEncrypted !== undefined
9379
- ? { credentialEncrypted: input.credentialEncrypted }
9380
- : {}),
9381
- ...(input.status !== undefined ? { status: input.status } : {}),
9382
- ...(input.tokenMetadata !== undefined ? { tokenMetadata: input.tokenMetadata } : {}),
9383
- updatedAt: new Date(),
9384
- })
9385
- .where(
9386
- and(
9387
- eq(schema.socialConnections.workspaceId, input.workspaceId),
9388
- eq(schema.socialConnections.id, input.connectionId),
9389
- ),
9390
- )
9391
- .returning();
9392
- return row ? mapSocialConnection(row) : null;
9393
- });
9052
+ return await withSocialConnectionSubjectRls(
9053
+ db,
9054
+ input.workspaceId,
9055
+ input.subjectId,
9056
+ async (scopedDb) => {
9057
+ const [row] = await scopedDb
9058
+ .update(schema.socialConnections)
9059
+ .set({
9060
+ ...(input.credentialEncrypted !== undefined
9061
+ ? { credentialEncrypted: input.credentialEncrypted }
9062
+ : {}),
9063
+ ...(input.status !== undefined ? { status: input.status } : {}),
9064
+ ...(input.tokenMetadata !== undefined ? { tokenMetadata: input.tokenMetadata } : {}),
9065
+ updatedAt: new Date(),
9066
+ })
9067
+ .where(
9068
+ and(
9069
+ eq(schema.socialConnections.workspaceId, input.workspaceId),
9070
+ eq(schema.socialConnections.id, input.connectionId),
9071
+ socialConnectionSubjectVisibility(input.subjectId),
9072
+ ),
9073
+ )
9074
+ .returning();
9075
+ return row ? mapSocialConnection(row) : null;
9076
+ },
9077
+ );
9394
9078
  }
9395
9079
 
9396
9080
  // List/get never select credential_encrypted (same posture as the broker
@@ -9403,12 +9087,18 @@ export async function listSocialConnections(
9403
9087
  db: Database,
9404
9088
  workspaceId: string,
9405
9089
  limit = 100,
9090
+ subjectId?: string | null,
9406
9091
  ): Promise<SocialConnection[]> {
9407
- return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
9092
+ return await withSocialConnectionSubjectRls(db, workspaceId, subjectId, async (scopedDb) => {
9408
9093
  const rows = await scopedDb
9409
9094
  .select(socialConnectionPublicColumns)
9410
9095
  .from(schema.socialConnections)
9411
- .where(eq(schema.socialConnections.workspaceId, workspaceId))
9096
+ .where(
9097
+ and(
9098
+ eq(schema.socialConnections.workspaceId, workspaceId),
9099
+ socialConnectionSubjectVisibility(subjectId),
9100
+ ),
9101
+ )
9412
9102
  .orderBy(desc(schema.socialConnections.createdAt))
9413
9103
  .limit(limit);
9414
9104
  return rows.map(mapSocialConnection);
@@ -9419,8 +9109,9 @@ export async function getSocialConnection(
9419
9109
  db: Database,
9420
9110
  workspaceId: string,
9421
9111
  connectionId: string,
9112
+ subjectId?: string | null,
9422
9113
  ): Promise<SocialConnection | null> {
9423
- return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
9114
+ return await withSocialConnectionSubjectRls(db, workspaceId, subjectId, async (scopedDb) => {
9424
9115
  const [row] = await scopedDb
9425
9116
  .select(socialConnectionPublicColumns)
9426
9117
  .from(schema.socialConnections)
@@ -9428,6 +9119,7 @@ export async function getSocialConnection(
9428
9119
  and(
9429
9120
  eq(schema.socialConnections.workspaceId, workspaceId),
9430
9121
  eq(schema.socialConnections.id, connectionId),
9122
+ socialConnectionSubjectVisibility(subjectId),
9431
9123
  ),
9432
9124
  )
9433
9125
  .limit(1);
@@ -9439,8 +9131,9 @@ export async function requireSocialConnection(
9439
9131
  db: Database,
9440
9132
  workspaceId: string,
9441
9133
  connectionId: string,
9134
+ subjectId?: string | null,
9442
9135
  ): Promise<SocialConnection> {
9443
- const connection = await getSocialConnection(db, workspaceId, connectionId);
9136
+ const connection = await getSocialConnection(db, workspaceId, connectionId, subjectId);
9444
9137
  if (!connection) {
9445
9138
  throw new Error(`Social connection not found: ${connectionId}`);
9446
9139
  }
@@ -9455,10 +9148,12 @@ export async function createSocialPost(
9455
9148
  db,
9456
9149
  { accountId: input.accountId, workspaceId: input.workspaceId },
9457
9150
  async (scopedDb) => {
9151
+ if (input.subjectId) await setSubjectRlsContext(scopedDb, input.subjectId);
9458
9152
  const connection = await requireSocialConnection(
9459
9153
  scopedDb,
9460
9154
  input.workspaceId,
9461
9155
  input.connectionId,
9156
+ input.subjectId,
9462
9157
  );
9463
9158
  const [row] = await scopedDb
9464
9159
  .insert(schema.socialPosts)
@@ -9495,6 +9190,7 @@ export async function recordSyncedSocialPosts(
9495
9190
  accountId: string;
9496
9191
  workspaceId: string;
9497
9192
  connectionId: string;
9193
+ subjectId?: string | null;
9498
9194
  posts: Array<{
9499
9195
  externalPostId: string;
9500
9196
  url?: string | null;
@@ -9513,10 +9209,12 @@ export async function recordSyncedSocialPosts(
9513
9209
  db,
9514
9210
  { accountId: input.accountId, workspaceId: input.workspaceId },
9515
9211
  async (scopedDb) => {
9212
+ if (input.subjectId) await setSubjectRlsContext(scopedDb, input.subjectId);
9516
9213
  const connection = await requireSocialConnection(
9517
9214
  scopedDb,
9518
9215
  input.workspaceId,
9519
9216
  input.connectionId,
9217
+ input.subjectId,
9520
9218
  );
9521
9219
  const rows = await scopedDb
9522
9220
  .insert(schema.socialPosts)
@@ -9552,6 +9250,7 @@ export async function listSocialPosts(
9552
9250
  db: Database,
9553
9251
  options: {
9554
9252
  workspaceId: string;
9253
+ subjectId?: string | null;
9555
9254
  connectionIds?: string[];
9556
9255
  since?: Date;
9557
9256
  limit?: number;
@@ -9565,15 +9264,20 @@ export async function listSocialPosts(
9565
9264
  conditions.push(gte(schema.socialPosts.publishedAt, options.since));
9566
9265
  }
9567
9266
  const limit = options.limit ?? 100;
9568
- return await withWorkspaceRls(db, options.workspaceId, async (scopedDb) => {
9569
- const rows = await scopedDb
9570
- .select()
9571
- .from(schema.socialPosts)
9572
- .where(and(...conditions))
9573
- .orderBy(desc(schema.socialPosts.publishedAt))
9574
- .limit(limit);
9575
- return rows.map(mapSocialPost);
9576
- });
9267
+ return await withSocialConnectionSubjectRls(
9268
+ db,
9269
+ options.workspaceId,
9270
+ options.subjectId,
9271
+ async (scopedDb) => {
9272
+ const rows = await scopedDb
9273
+ .select()
9274
+ .from(schema.socialPosts)
9275
+ .where(and(...conditions))
9276
+ .orderBy(desc(schema.socialPosts.publishedAt))
9277
+ .limit(limit);
9278
+ return rows.map(mapSocialPost);
9279
+ },
9280
+ );
9577
9281
  }
9578
9282
 
9579
9283
  export async function createScheduledTask(
@@ -11646,27 +11350,6 @@ export type WorkspaceEnvironmentForRun = VariableSetForRun;
11646
11350
  // `getCodexCredentialStatus` returns metadata only (never the secret column).
11647
11351
  // ---------------------------------------------------------------------------
11648
11352
 
11649
- export type CodexCredentialTokens = {
11650
- accessToken: string;
11651
- refreshToken: string;
11652
- idToken: string;
11653
- };
11654
-
11655
- export type CodexCredentialForRun = {
11656
- id: string; // row id — for compare-and-set writes (P1-c)
11657
- version: number; // optimistic-concurrency version loaded with this snapshot
11658
- workspaceId: string;
11659
- tokens: CodexCredentialTokens; // decrypted — never logged, never returned by a route
11660
- chatgptAccountId: string | null;
11661
- scopes: string | null;
11662
- planType: string | null;
11663
- isFedramp: boolean;
11664
- expiresAt: Date | null;
11665
- lastRefreshAt: Date | null;
11666
- status: string;
11667
- lastError: string | null;
11668
- };
11669
-
11670
11353
  /**
11671
11354
  * Login / rotation write (multi-account P1). Caller passes the PRE-encrypted
11672
11355
  * credential blob. Keyed on the composite partial index (workspace, chatgpt
@@ -11790,11 +11473,10 @@ export async function upsertCodexSubscriptionCredential(
11790
11473
  // it when still null) so a re-connect never clobbers a rename.
11791
11474
  accountEmail: input.accountEmail ?? null,
11792
11475
  label: sql`coalesce(${schema.codexSubscriptionCredentials.label}, ${input.label ?? null})`,
11793
- // Ownership follows the most recent connection exactly. A
11794
- // configured/delegated/API-key reconnect is intentionally
11795
- // nonhuman and clears the prior human owner, making the row
11796
- // view-only until a direct managed-cookie human reconnects it.
11797
- connectedBySubjectId: input.connectedBySubjectId ?? null,
11476
+ // Reconnect refreshes credential material, never ownership. A row
11477
+ // without an owner may be claimed by its first direct managed human;
11478
+ // after that, disconnect is the explicit ownership-reset boundary.
11479
+ connectedBySubjectId: sql`coalesce(${schema.codexSubscriptionCredentials.connectedBySubjectId}, ${input.connectedBySubjectId ?? null})`,
11798
11480
  status: "active",
11799
11481
  lastError: null,
11800
11482
  version: sql`${schema.codexSubscriptionCredentials.version} + 1`,
@@ -11820,6 +11502,333 @@ export async function upsertCodexSubscriptionCredential(
11820
11502
  );
11821
11503
  }
11822
11504
 
11505
+ export type CodexAppsSettings = {
11506
+ credentialId: string | null;
11507
+ version: number;
11508
+ designatedAt: Date | null;
11509
+ };
11510
+
11511
+ export async function getCodexAppsSettings(
11512
+ db: Database,
11513
+ workspaceId: string,
11514
+ ): Promise<CodexAppsSettings> {
11515
+ return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
11516
+ const [row] = await scopedDb
11517
+ .select({
11518
+ credentialId: schema.codexAppsSettings.credentialId,
11519
+ version: schema.codexAppsSettings.version,
11520
+ designatedAt: schema.codexAppsSettings.designatedAt,
11521
+ })
11522
+ .from(schema.codexAppsSettings)
11523
+ .where(eq(schema.codexAppsSettings.workspaceId, workspaceId))
11524
+ .limit(1);
11525
+ return row ?? { credentialId: null, version: 0, designatedAt: null };
11526
+ });
11527
+ }
11528
+
11529
+ export type CodexAppsCredentialAuthorization = {
11530
+ credentialId: string;
11531
+ ownerSubjectId: string;
11532
+ };
11533
+
11534
+ function canManageCodexApps(permissions: unknown): boolean {
11535
+ return (
11536
+ Array.isArray(permissions) &&
11537
+ (permissions.includes("connections:write") || permissions.includes("workspace:admin"))
11538
+ );
11539
+ }
11540
+
11541
+ export class CodexAppsAuthorizationRevokedError extends Error {
11542
+ constructor() {
11543
+ super("Codex Apps authorization is no longer active");
11544
+ this.name = "CodexAppsAuthorizationRevokedError";
11545
+ }
11546
+ }
11547
+
11548
+ /** Exact Apps credential and owner, independent of every inference-capacity field. */
11549
+ export async function getCodexAppsCredentialAuthorizationForRun(
11550
+ db: Database,
11551
+ workspaceId: string,
11552
+ ): Promise<CodexAppsCredentialAuthorization | null> {
11553
+ return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
11554
+ const [row] = await scopedDb
11555
+ .select({
11556
+ credentialId: schema.codexAppsSettings.credentialId,
11557
+ ownerSubjectId: schema.codexSubscriptionCredentials.connectedBySubjectId,
11558
+ })
11559
+ .from(schema.codexAppsSettings)
11560
+ .innerJoin(
11561
+ schema.codexSubscriptionCredentials,
11562
+ and(
11563
+ eq(schema.codexSubscriptionCredentials.id, schema.codexAppsSettings.credentialId),
11564
+ eq(schema.codexSubscriptionCredentials.accountId, schema.codexAppsSettings.accountId),
11565
+ eq(schema.codexSubscriptionCredentials.workspaceId, workspaceId),
11566
+ eq(schema.codexSubscriptionCredentials.status, "active"),
11567
+ ),
11568
+ )
11569
+ .where(eq(schema.codexAppsSettings.workspaceId, workspaceId))
11570
+ .limit(1);
11571
+ return row?.credentialId && row.ownerSubjectId
11572
+ ? { credentialId: row.credentialId, ownerSubjectId: row.ownerSubjectId }
11573
+ : null;
11574
+ });
11575
+ }
11576
+
11577
+ /**
11578
+ * Hold the exact designation, credential, and owner-membership authorization
11579
+ * stable through one outbound Apps request. Clear/disconnect operations share
11580
+ * the workspace advisory lock; credential or membership revocation must wait on
11581
+ * the row locks. The callback deliberately runs inside this short transaction so
11582
+ * no revocation can commit in the gap between a final check and network dispatch.
11583
+ */
11584
+ export async function withCodexAppsRequestAuthorization<T>(
11585
+ db: Database,
11586
+ input: { workspaceId: string; credentialId: string },
11587
+ use: () => Promise<T>,
11588
+ ): Promise<T> {
11589
+ return await withWorkspaceRls(db, input.workspaceId, async (scopedDb) => {
11590
+ await scopedDb.execute(
11591
+ sql`select pg_advisory_xact_lock(hashtextextended(${`codex-apps-settings:${input.workspaceId}`}, 0))`,
11592
+ );
11593
+ const [designation] = await scopedDb
11594
+ .select({ credentialId: schema.codexAppsSettings.credentialId })
11595
+ .from(schema.codexAppsSettings)
11596
+ .where(eq(schema.codexAppsSettings.workspaceId, input.workspaceId))
11597
+ .for("share")
11598
+ .limit(1);
11599
+ if (designation?.credentialId !== input.credentialId) {
11600
+ throw new CodexAppsAuthorizationRevokedError();
11601
+ }
11602
+ const [credential] = await scopedDb
11603
+ .select({
11604
+ ownerSubjectId: schema.codexSubscriptionCredentials.connectedBySubjectId,
11605
+ status: schema.codexSubscriptionCredentials.status,
11606
+ })
11607
+ .from(schema.codexSubscriptionCredentials)
11608
+ .where(
11609
+ and(
11610
+ eq(schema.codexSubscriptionCredentials.workspaceId, input.workspaceId),
11611
+ eq(schema.codexSubscriptionCredentials.id, input.credentialId),
11612
+ ),
11613
+ )
11614
+ .for("share")
11615
+ .limit(1);
11616
+ if (!credential?.ownerSubjectId || credential.status !== "active") {
11617
+ throw new CodexAppsAuthorizationRevokedError();
11618
+ }
11619
+ const [membership] = await scopedDb
11620
+ .select({ permissions: schema.workspaceMemberships.permissions })
11621
+ .from(schema.workspaceMemberships)
11622
+ .where(
11623
+ and(
11624
+ eq(schema.workspaceMemberships.workspaceId, input.workspaceId),
11625
+ eq(schema.workspaceMemberships.subjectId, credential.ownerSubjectId),
11626
+ ),
11627
+ )
11628
+ .for("share")
11629
+ .limit(1);
11630
+ if (!canManageCodexApps(membership?.permissions)) {
11631
+ throw new CodexAppsAuthorizationRevokedError();
11632
+ }
11633
+ return await use();
11634
+ });
11635
+ }
11636
+
11637
+ export type DesignateCodexAppsCredentialResult =
11638
+ | ({ kind: "updated" } & CodexAppsSettings)
11639
+ | ({ kind: "conflict" | "already_designated" } & CodexAppsSettings)
11640
+ | { kind: "not_found" }
11641
+ | { kind: "not_owner" }
11642
+ | { kind: "forbidden" }
11643
+ | { kind: "unavailable" };
11644
+
11645
+ export async function designateCodexAppsCredential(
11646
+ db: Database,
11647
+ input: {
11648
+ accountId: string;
11649
+ workspaceId: string;
11650
+ credentialId: string;
11651
+ subjectId: string;
11652
+ expectedVersion: number;
11653
+ },
11654
+ ): Promise<DesignateCodexAppsCredentialResult> {
11655
+ return await withRlsContext(
11656
+ db,
11657
+ { accountId: input.accountId, workspaceId: input.workspaceId },
11658
+ async (scopedDb) => {
11659
+ await scopedDb.execute(
11660
+ sql`select pg_advisory_xact_lock(hashtextextended(${`codex-apps-settings:${input.workspaceId}`}, 0))`,
11661
+ );
11662
+ const [currentRow] = await scopedDb
11663
+ .select()
11664
+ .from(schema.codexAppsSettings)
11665
+ .where(eq(schema.codexAppsSettings.workspaceId, input.workspaceId))
11666
+ .for("update")
11667
+ .limit(1);
11668
+ const current: CodexAppsSettings = currentRow
11669
+ ? {
11670
+ credentialId: currentRow.credentialId,
11671
+ version: currentRow.version,
11672
+ designatedAt: currentRow.designatedAt,
11673
+ }
11674
+ : { credentialId: null, version: 0, designatedAt: null };
11675
+ if (current.version !== input.expectedVersion) return { kind: "conflict", ...current };
11676
+ if (current.credentialId !== null) return { kind: "already_designated", ...current };
11677
+
11678
+ const [credential] = await scopedDb
11679
+ .select({
11680
+ connectedBySubjectId: schema.codexSubscriptionCredentials.connectedBySubjectId,
11681
+ status: schema.codexSubscriptionCredentials.status,
11682
+ })
11683
+ .from(schema.codexSubscriptionCredentials)
11684
+ .where(
11685
+ and(
11686
+ eq(schema.codexSubscriptionCredentials.accountId, input.accountId),
11687
+ eq(schema.codexSubscriptionCredentials.workspaceId, input.workspaceId),
11688
+ eq(schema.codexSubscriptionCredentials.id, input.credentialId),
11689
+ ),
11690
+ )
11691
+ .for("update")
11692
+ .limit(1);
11693
+ if (!credential) return { kind: "not_found" };
11694
+ if (credential.connectedBySubjectId !== input.subjectId) return { kind: "not_owner" };
11695
+ if (credential.status !== "active") return { kind: "unavailable" };
11696
+
11697
+ const [membership] = await scopedDb
11698
+ .select({ permissions: schema.workspaceMemberships.permissions })
11699
+ .from(schema.workspaceMemberships)
11700
+ .where(
11701
+ and(
11702
+ eq(schema.workspaceMemberships.accountId, input.accountId),
11703
+ eq(schema.workspaceMemberships.workspaceId, input.workspaceId),
11704
+ eq(schema.workspaceMemberships.subjectId, input.subjectId),
11705
+ ),
11706
+ )
11707
+ .for("update")
11708
+ .limit(1);
11709
+ if (!canManageCodexApps(membership?.permissions)) return { kind: "forbidden" };
11710
+
11711
+ const now = new Date();
11712
+ const version = current.version + 1;
11713
+ const [updated] = await scopedDb
11714
+ .insert(schema.codexAppsSettings)
11715
+ .values({
11716
+ accountId: input.accountId,
11717
+ workspaceId: input.workspaceId,
11718
+ credentialId: input.credentialId,
11719
+ version,
11720
+ designatedAt: now,
11721
+ updatedAt: now,
11722
+ })
11723
+ .onConflictDoUpdate({
11724
+ target: schema.codexAppsSettings.workspaceId,
11725
+ set: {
11726
+ credentialId: input.credentialId,
11727
+ version,
11728
+ designatedAt: now,
11729
+ updatedAt: now,
11730
+ },
11731
+ })
11732
+ .returning({
11733
+ credentialId: schema.codexAppsSettings.credentialId,
11734
+ version: schema.codexAppsSettings.version,
11735
+ designatedAt: schema.codexAppsSettings.designatedAt,
11736
+ });
11737
+ if (!updated?.credentialId || !updated.designatedAt) {
11738
+ throw new Error("Codex Apps designation was not persisted");
11739
+ }
11740
+ await scopedDb.insert(schema.auditEvents).values({
11741
+ accountId: input.accountId,
11742
+ workspaceId: input.workspaceId,
11743
+ subjectId: input.subjectId,
11744
+ action: "codex_apps.designated",
11745
+ targetType: "codex_subscription_credential",
11746
+ targetId: input.credentialId,
11747
+ metadata: { version },
11748
+ });
11749
+ return { kind: "updated", ...updated };
11750
+ },
11751
+ );
11752
+ }
11753
+
11754
+ export type ClearCodexAppsCredentialResult = {
11755
+ kind: "updated" | "unchanged" | "conflict" | "forbidden";
11756
+ } & CodexAppsSettings;
11757
+
11758
+ export async function clearCodexAppsCredential(
11759
+ db: Database,
11760
+ input: {
11761
+ accountId: string;
11762
+ workspaceId: string;
11763
+ subjectId: string;
11764
+ expectedVersion: number;
11765
+ },
11766
+ ): Promise<ClearCodexAppsCredentialResult> {
11767
+ return await withRlsContext(
11768
+ db,
11769
+ { accountId: input.accountId, workspaceId: input.workspaceId },
11770
+ async (scopedDb) => {
11771
+ await scopedDb.execute(
11772
+ sql`select pg_advisory_xact_lock(hashtextextended(${`codex-apps-settings:${input.workspaceId}`}, 0))`,
11773
+ );
11774
+ const [row] = await scopedDb
11775
+ .select()
11776
+ .from(schema.codexAppsSettings)
11777
+ .where(eq(schema.codexAppsSettings.workspaceId, input.workspaceId))
11778
+ .for("update")
11779
+ .limit(1);
11780
+ const current: CodexAppsSettings = row
11781
+ ? { credentialId: row.credentialId, version: row.version, designatedAt: row.designatedAt }
11782
+ : { credentialId: null, version: 0, designatedAt: null };
11783
+ const [membership] = await scopedDb
11784
+ .select({ permissions: schema.workspaceMemberships.permissions })
11785
+ .from(schema.workspaceMemberships)
11786
+ .where(
11787
+ and(
11788
+ eq(schema.workspaceMemberships.accountId, input.accountId),
11789
+ eq(schema.workspaceMemberships.workspaceId, input.workspaceId),
11790
+ eq(schema.workspaceMemberships.subjectId, input.subjectId),
11791
+ ),
11792
+ )
11793
+ .for("update")
11794
+ .limit(1);
11795
+ if (!canManageCodexApps(membership?.permissions)) {
11796
+ return { kind: "forbidden", ...current };
11797
+ }
11798
+ if (current.version !== input.expectedVersion) return { kind: "conflict", ...current };
11799
+ if (current.credentialId === null) return { kind: "unchanged", ...current };
11800
+
11801
+ const now = new Date();
11802
+ const version = current.version + 1;
11803
+ const [updated] = await scopedDb
11804
+ .update(schema.codexAppsSettings)
11805
+ .set({
11806
+ credentialId: null,
11807
+ version,
11808
+ designatedAt: null,
11809
+ updatedAt: now,
11810
+ })
11811
+ .where(eq(schema.codexAppsSettings.workspaceId, input.workspaceId))
11812
+ .returning({
11813
+ credentialId: schema.codexAppsSettings.credentialId,
11814
+ version: schema.codexAppsSettings.version,
11815
+ designatedAt: schema.codexAppsSettings.designatedAt,
11816
+ });
11817
+ if (!updated) throw new Error("Codex Apps designation clear was not persisted");
11818
+ await scopedDb.insert(schema.auditEvents).values({
11819
+ accountId: input.accountId,
11820
+ workspaceId: input.workspaceId,
11821
+ subjectId: input.subjectId,
11822
+ action: "codex_apps.cleared",
11823
+ targetType: "codex_subscription_credential",
11824
+ targetId: current.credentialId,
11825
+ metadata: { version },
11826
+ });
11827
+ return { kind: "updated", ...updated };
11828
+ },
11829
+ );
11830
+ }
11831
+
11823
11832
  /**
11824
11833
  * The ONLY decrypt-read accessor. Fails closed. Never call from an API route that
11825
11834
  * returns the result.
@@ -12306,11 +12315,6 @@ export type CodexAccountStatus = {
12306
12315
  // P3 rotation cooldown: when set and in the future, this account is cooling-down
12307
12316
  // (rotated-off after a usage cap) and the engine skips it. null ⇒ not cooling.
12308
12317
  exhaustedUntil: Date | null;
12309
- // P4 connector-aware rotation: the ORIGINAL-dotted connector namespaces this
12310
- // account exposes via codex_apps (github/gmail/linear/…). null ⇒ never probed
12311
- // (the ranker treats it as unknown: never credited as covering, never excluded).
12312
- connectorNamespaces: string[] | null;
12313
- connectorsCheckedAt: Date | null;
12314
12318
  };
12315
12319
 
12316
12320
  /**
@@ -12429,8 +12433,6 @@ type CodexLeaseCandidateRow = {
12429
12433
  secondary_reset_at: Date | string | null;
12430
12434
  usage_checked_at: Date | string | null;
12431
12435
  exhausted_until: Date | string | null;
12432
- connector_namespaces: string[] | null;
12433
- connectors_checked_at: Date | string | null;
12434
12436
  selection_count: number;
12435
12437
  last_selected_at: Date | string | null;
12436
12438
  active_lease_count: number;
@@ -12463,8 +12465,6 @@ function mapCodexLeaseCandidate(
12463
12465
  secondaryResetAt: codexMetadataDate(row.secondary_reset_at),
12464
12466
  usageCheckedAt: codexMetadataDate(row.usage_checked_at),
12465
12467
  exhaustedUntil: codexMetadataDate(row.exhausted_until),
12466
- connectorNamespaces: row.connector_namespaces,
12467
- connectorsCheckedAt: codexMetadataDate(row.connectors_checked_at),
12468
12468
  selectionCount: Number(row.selection_count),
12469
12469
  lastSelectedAt: codexMetadataDate(row.last_selected_at),
12470
12470
  activeLeaseCount: Number(row.active_lease_count),
@@ -12524,8 +12524,6 @@ async function listCodexLeaseCandidatesInTransaction(
12524
12524
  c.secondary_reset_at,
12525
12525
  c.usage_checked_at,
12526
12526
  c.exhausted_until,
12527
- c.connector_namespaces,
12528
- c.connectors_checked_at,
12529
12527
  c.selection_count,
12530
12528
  c.last_selected_at,
12531
12529
  count(l.id) filter (
@@ -12572,8 +12570,6 @@ export async function acquireCodexCredentialLease<
12572
12570
  holderId: string;
12573
12571
  /** Pins must not move the workspace-global cursor. */
12574
12572
  advanceActivePointer: boolean;
12575
- /** Exact frozen credential for this same durable turn, if it is resuming. */
12576
- continuationCredentialId?: string | null;
12577
12573
  /**
12578
12574
  * Optional downstream parser for private accepted-turn policy metadata.
12579
12575
  * It is pure, runs under the turn/rotation transaction, and must not query
@@ -12582,8 +12578,8 @@ export async function acquireCodexCredentialLease<
12582
12578
  resolvePolicyScope?: CodexCredentialLeasePolicyScopeResolver<TPolicyScope>;
12583
12579
  /**
12584
12580
  * Optional downstream membership policy for NEW allocations only. A live
12585
- * lease or validated frozen credential is offered to the selector against
12586
- * the complete workspace rows first and can never be filtered out here.
12581
+ * exact-turn lease is offered to the selector against the complete workspace
12582
+ * rows first and can never be filtered out here.
12587
12583
  */
12588
12584
  filterNewAllocationCandidates?: CodexCredentialLeaseCandidateFilter<
12589
12585
  TPolicyScope,
@@ -12649,23 +12645,6 @@ export async function acquireCodexCredentialLease<
12649
12645
  throw new Error(`Session turn not found for Codex lease: ${input.turnId}`);
12650
12646
  }
12651
12647
  const policyScope = input.resolvePolicyScope?.(turns[0].metadata ?? {}) ?? null;
12652
- const continuationRows = input.continuationCredentialId
12653
- ? await tx.execute(
12654
- sql<{ frozen_codex_credential_id: string | null }>`
12655
- select frozen_codex_credential_id
12656
- from agent_run_states
12657
- where account_id = ${input.accountId}
12658
- and workspace_id = ${input.workspaceId}
12659
- and turn_id = ${input.turnId}
12660
- order by state_version desc
12661
- limit 1
12662
- `,
12663
- )
12664
- : [];
12665
- const validatedContinuationCredentialId =
12666
- continuationRows[0]?.frozen_codex_credential_id === input.continuationCredentialId
12667
- ? input.continuationCredentialId
12668
- : null;
12669
12648
  const activeCredentialId = settingsRow.active_credential_id;
12670
12649
  const rotationEnabled = settingsRow.rotation_enabled;
12671
12650
  // Fail closed on a torn/manual legacy write. The user-intent bit and the
@@ -12707,7 +12686,7 @@ export async function acquireCodexCredentialLease<
12707
12686
  activeCredentialId,
12708
12687
  excludeTurnId: input.turnId,
12709
12688
  });
12710
- const sameTurnCredentialId = existingCredentialId ?? validatedContinuationCredentialId;
12689
+ const sameTurnCredentialId = existingCredentialId;
12711
12690
  const selectionContext = (
12712
12691
  accounts: CodexLeaseAccountStatus[],
12713
12692
  unavailableDiagnostics: readonly TUnavailableDiagnostic[],
@@ -12731,10 +12710,9 @@ export async function acquireCodexCredentialLease<
12731
12710
  }
12732
12711
  }
12733
12712
 
12734
- // Exact-turn continuity is resolved before any future pool membership
12713
+ // A live exact-turn lease is resolved before any future pool membership
12735
12714
  // filter. The normal selector still owns health validation: a quarantined
12736
- // live/frozen row falls through to scoped new acquisition rather than
12737
- // being reused blindly.
12715
+ // row falls through to scoped new acquisition rather than being reused.
12738
12716
  if (!selected) {
12739
12717
  const filtered = filterCodexLeaseCandidatesForPolicy(
12740
12718
  allAccounts,
@@ -12771,11 +12749,7 @@ export async function acquireCodexCredentialLease<
12771
12749
  if (!selectedAccount) {
12772
12750
  throw new Error("Codex lease selector returned a credential outside the workspace pool");
12773
12751
  }
12774
- if (
12775
- !selectedAccount.allocatorEnabled &&
12776
- selectedAccount.id !== existingCredentialId &&
12777
- selectedAccount.id !== validatedContinuationCredentialId
12778
- ) {
12752
+ if (!selectedAccount.allocatorEnabled && selectedAccount.id !== existingCredentialId) {
12779
12753
  throw new Error("Codex lease selector returned a credential disabled for new allocations");
12780
12754
  }
12781
12755
 
@@ -14190,9 +14164,6 @@ export async function listCodexAccountStatuses(
14190
14164
  secondaryResetAt: schema.codexSubscriptionCredentials.secondaryResetAt,
14191
14165
  usageCheckedAt: schema.codexSubscriptionCredentials.usageCheckedAt,
14192
14166
  exhaustedUntil: schema.codexSubscriptionCredentials.exhaustedUntil,
14193
- // P4 connector-set cache — metadata-only, rides along on this read.
14194
- connectorNamespaces: schema.codexSubscriptionCredentials.connectorNamespaces,
14195
- connectorsCheckedAt: schema.codexSubscriptionCredentials.connectorsCheckedAt,
14196
14167
  })
14197
14168
  .from(schema.codexSubscriptionCredentials)
14198
14169
  .where(eq(schema.codexSubscriptionCredentials.workspaceId, workspaceId))
@@ -14210,7 +14181,6 @@ export async function listCodexAccountStatuses(
14210
14181
  secondaryResetAt: codexMetadataDate(row.secondaryResetAt),
14211
14182
  usageCheckedAt: codexMetadataDate(row.usageCheckedAt),
14212
14183
  exhaustedUntil: codexMetadataDate(row.exhaustedUntil),
14213
- connectorsCheckedAt: codexMetadataDate(row.connectorsCheckedAt),
14214
14184
  isActive: row.id === activeId,
14215
14185
  }));
14216
14186
  });
@@ -15111,16 +15081,6 @@ export async function completeCodexResetRedemption(
15111
15081
  }
15112
15082
 
15113
15083
  /** The P2 usage-cache snapshot written by the refreshing usage wrapper. */
15114
- export type CodexAccountUsageSnapshot = {
15115
- primaryUsedPercent?: number | null;
15116
- primaryResetAt?: Date | null;
15117
- secondaryUsedPercent?: number | null;
15118
- secondaryResetAt?: Date | null;
15119
- /** Present only when the quota body parsed successfully. */
15120
- checkedAt?: Date;
15121
- resetCreditAvailableCount?: number | null;
15122
- resetCreditsCheckedAt?: Date | null;
15123
- };
15124
15084
 
15125
15085
  /**
15126
15086
  * Cache-write for P2 quota bars: persist the five plaintext usage columns on a
@@ -15483,46 +15443,6 @@ export async function countConsecutiveReactiveRotations(
15483
15443
  });
15484
15444
  }
15485
15445
 
15486
- /**
15487
- * P4 connector-set cache writer: persist the set of ORIGINAL-dotted connector
15488
- * namespaces a SPECIFIC credential exposes via codex_apps (+ the freshness clock).
15489
- * Modeled byte-for-byte on recordCodexAccountUsage / setCodexCredentialExhausted:
15490
- * RLS-scoped, guarded by (id, workspace_id), and — critically — NO `version` bump and
15491
- * NO `updatedAt` touch, so it can never race the (id, version) token-refresh CAS.
15492
- *
15493
- * The CALLER must only invoke this with a NON-EMPTY set: codex_apps connects
15494
- * best-effort (a transient failure yields an empty tools/list), and overwriting a
15495
- * known non-empty set with [] would falsely "drop" coverage on a flaky turn. A
15496
- * genuinely connector-less account stays null (the ranker treats null as unknown).
15497
- * Returns true iff a row was updated (false ⇒ the credential was disconnected under us).
15498
- */
15499
- export async function recordCodexAccountConnectors(
15500
- db: Database,
15501
- workspaceId: string,
15502
- credentialId: string,
15503
- namespaces: string[],
15504
- ): Promise<boolean> {
15505
- return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
15506
- const updated = await scopedDb
15507
- .update(schema.codexSubscriptionCredentials)
15508
- .set({
15509
- connectorNamespaces: namespaces,
15510
- connectorsCheckedAt: new Date(),
15511
- // NB: no `version` bump and no `updatedAt` touch — connector set is non-credential
15512
- // metadata and must NOT race the (id, version) refresh CAS (same discipline as
15513
- // recordCodexAccountUsage / setCodexCredentialExhausted).
15514
- })
15515
- .where(
15516
- and(
15517
- eq(schema.codexSubscriptionCredentials.id, credentialId),
15518
- eq(schema.codexSubscriptionCredentials.workspaceId, workspaceId),
15519
- ),
15520
- )
15521
- .returning({ id: schema.codexSubscriptionCredentials.id });
15522
- return updated.length > 0;
15523
- });
15524
- }
15525
-
15526
15446
  /** The supported rotation strategies (P3). */
15527
15447
  export const CODEX_ROTATION_STRATEGIES = [
15528
15448
  "most_remaining",
@@ -15733,6 +15653,7 @@ export async function disconnectCodexAccount(
15733
15653
  db: Database,
15734
15654
  workspaceId: string,
15735
15655
  credentialId: string,
15656
+ actorSubjectId: string | null = null,
15736
15657
  ): Promise<{
15737
15658
  removed: boolean;
15738
15659
  newActiveCredentialId: string | null;
@@ -15744,8 +15665,20 @@ export async function disconnectCodexAccount(
15744
15665
  where workspace_id = ${workspaceId}
15745
15666
  for update
15746
15667
  `);
15668
+ await scopedDb.execute(
15669
+ sql`select pg_advisory_xact_lock(hashtextextended(${`codex-apps-settings:${workspaceId}`}, 0))`,
15670
+ );
15671
+ const [appsSettings] = await scopedDb
15672
+ .select()
15673
+ .from(schema.codexAppsSettings)
15674
+ .where(eq(schema.codexAppsSettings.workspaceId, workspaceId))
15675
+ .for("update")
15676
+ .limit(1);
15747
15677
  const [credential] = await scopedDb
15748
- .select({ id: schema.codexSubscriptionCredentials.id })
15678
+ .select({
15679
+ id: schema.codexSubscriptionCredentials.id,
15680
+ accountId: schema.codexSubscriptionCredentials.accountId,
15681
+ })
15749
15682
  .from(schema.codexSubscriptionCredentials)
15750
15683
  .where(
15751
15684
  and(
@@ -15787,6 +15720,27 @@ export async function disconnectCodexAccount(
15787
15720
  blockedByUnresolvedRedemption: true,
15788
15721
  };
15789
15722
  }
15723
+ if (appsSettings?.credentialId === credentialId) {
15724
+ const version = appsSettings.version + 1;
15725
+ await scopedDb
15726
+ .update(schema.codexAppsSettings)
15727
+ .set({
15728
+ credentialId: null,
15729
+ version,
15730
+ designatedAt: null,
15731
+ updatedAt: new Date(),
15732
+ })
15733
+ .where(eq(schema.codexAppsSettings.id, appsSettings.id));
15734
+ await scopedDb.insert(schema.auditEvents).values({
15735
+ accountId: credential.accountId,
15736
+ workspaceId,
15737
+ subjectId: actorSubjectId,
15738
+ action: "codex_apps.cleared_on_disconnect",
15739
+ targetType: "codex_subscription_credential",
15740
+ targetId: credentialId,
15741
+ metadata: { version },
15742
+ });
15743
+ }
15790
15744
  const removedRows = await scopedDb
15791
15745
  .delete(schema.codexSubscriptionCredentials)
15792
15746
  .where(
@@ -15839,10 +15793,23 @@ export async function disconnectCodexAccount(
15839
15793
  export async function disconnectAllCodexAccounts(
15840
15794
  db: Database,
15841
15795
  workspaceId: string,
15796
+ actorSubjectId: string | null = null,
15842
15797
  ): Promise<{ removed: number; blockedCredentialIds: string[] }> {
15843
15798
  return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
15799
+ await scopedDb.execute(
15800
+ sql`select pg_advisory_xact_lock(hashtextextended(${`codex-apps-settings:${workspaceId}`}, 0))`,
15801
+ );
15802
+ const [appsSettings] = await scopedDb
15803
+ .select()
15804
+ .from(schema.codexAppsSettings)
15805
+ .where(eq(schema.codexAppsSettings.workspaceId, workspaceId))
15806
+ .for("update")
15807
+ .limit(1);
15844
15808
  const credentials = await scopedDb
15845
- .select({ id: schema.codexSubscriptionCredentials.id })
15809
+ .select({
15810
+ id: schema.codexSubscriptionCredentials.id,
15811
+ accountId: schema.codexSubscriptionCredentials.accountId,
15812
+ })
15846
15813
  .from(schema.codexSubscriptionCredentials)
15847
15814
  .where(eq(schema.codexSubscriptionCredentials.workspaceId, workspaceId))
15848
15815
  .orderBy(asc(schema.codexSubscriptionCredentials.id))
@@ -15865,6 +15832,27 @@ export async function disconnectAllCodexAccounts(
15865
15832
  blockedCredentialIds: blocked.map((row) => row.credentialId).sort(),
15866
15833
  };
15867
15834
  }
15835
+ if (appsSettings?.credentialId) {
15836
+ const version = appsSettings.version + 1;
15837
+ await scopedDb
15838
+ .update(schema.codexAppsSettings)
15839
+ .set({
15840
+ credentialId: null,
15841
+ version,
15842
+ designatedAt: null,
15843
+ updatedAt: new Date(),
15844
+ })
15845
+ .where(eq(schema.codexAppsSettings.id, appsSettings.id));
15846
+ await scopedDb.insert(schema.auditEvents).values({
15847
+ accountId: credentials[0]!.accountId,
15848
+ workspaceId,
15849
+ subjectId: actorSubjectId,
15850
+ action: "codex_apps.cleared_on_disconnect",
15851
+ targetType: "codex_subscription_credential",
15852
+ targetId: appsSettings.credentialId,
15853
+ metadata: { version },
15854
+ });
15855
+ }
15868
15856
  const rows = await scopedDb
15869
15857
  .delete(schema.codexSubscriptionCredentials)
15870
15858
  .where(eq(schema.codexSubscriptionCredentials.workspaceId, workspaceId))
@@ -17444,6 +17432,18 @@ async function lockSessionCreateIdempotencyKey(
17444
17432
  );
17445
17433
  }
17446
17434
 
17435
+ async function lockAgentSessionCreate(tx: Database, input: SessionCreateInput): Promise<void> {
17436
+ const actorSessionId = input.createdByActor?.sessionId;
17437
+ if (!actorSessionId) return;
17438
+ // Agent-originated child creation first reads the parent under FOR SHARE,
17439
+ // then verifies the live attempt under FOR UPDATE. Serialize creates from
17440
+ // the same parent session so concurrent tool calls cannot deadlock while
17441
+ // upgrading that row lock. Unrelated sessions retain full concurrency.
17442
+ await tx.execute(
17443
+ sql`select pg_advisory_xact_lock(hashtextextended(${`agent-session-create:${input.workspaceId}:${actorSessionId}`}, 0))`,
17444
+ );
17445
+ }
17446
+
17447
17447
  async function existingSpawnDenialForKey(
17448
17448
  tx: Database,
17449
17449
  workspaceId: string,
@@ -17549,6 +17549,8 @@ async function createSessionInTransaction(
17549
17549
  }
17550
17550
  }
17551
17551
 
17552
+ await lockAgentSessionCreate(tx, input);
17553
+
17552
17554
  const decision = await resolveSessionDepthDecision(tx, input, id, workspace, deploymentPolicy);
17553
17555
  if (decision.denied) {
17554
17556
  return {
@@ -20577,11 +20579,6 @@ export async function getLatestRunState(
20577
20579
  turnId: string | null;
20578
20580
  serializedRunState: string;
20579
20581
  pendingApprovals: unknown[];
20580
- // The codex account that froze this state (pin > workspace-active), or null
20581
- // when frozen on the non-codex path / before the column existed. The replay
20582
- // path compares it to the resuming turn's codex account to decide whether the
20583
- // blob's account-bound reasoning must be neutralized before being replayed.
20584
- frozenCodexCredentialId: string | null;
20585
20582
  providerArtifactInvalidatedAt: Date | null;
20586
20583
  } | null> {
20587
20584
  return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
@@ -20602,7 +20599,6 @@ export async function getLatestRunState(
20602
20599
  turnId: row.turnId ?? null,
20603
20600
  serializedRunState: row.serializedRunState,
20604
20601
  pendingApprovals: row.pendingApprovals,
20605
- frozenCodexCredentialId: row.frozenCodexCredentialId ?? null,
20606
20602
  providerArtifactInvalidatedAt: row.providerArtifactInvalidatedAt ?? null,
20607
20603
  }
20608
20604
  : null;
@@ -21359,10 +21355,6 @@ export async function appendSessionHistoryItems(
21359
21355
  turnId: string;
21360
21356
  expectedExecutionGeneration: number;
21361
21357
  expectedAttemptId: string;
21362
- // The codex account that produced these items (the turn's resolved credential
21363
- // id), or null/undefined on the non-codex path. Stored verbatim so the read
21364
- // path can strip cross-account reasoning.encrypted_content blobs per turn.
21365
- producerCodexCredentialId?: string | null;
21366
21358
  modelToolOutputTruncationTokens?: number;
21367
21359
  items: Array<{ position: number; item: Record<string, unknown> }>;
21368
21360
  },
@@ -21391,7 +21383,6 @@ export async function appendSessionHistoryItems(
21391
21383
  workspaceId: input.workspaceId,
21392
21384
  sessionId: input.sessionId,
21393
21385
  turnId: input.turnId,
21394
- producerCodexCredentialId: input.producerCodexCredentialId ?? null,
21395
21386
  position: entry.position,
21396
21387
  // This is the canonical model-memory boundary. The pending-call
21397
21388
  // ledger and audit event may retain their separate raw/preview
@@ -21770,18 +21761,18 @@ export async function getActiveSessionHistoryItems(
21770
21761
  sessionId: string,
21771
21762
  ): Promise<
21772
21763
  Array<{
21764
+ id: string;
21773
21765
  position: number;
21774
21766
  item: Record<string, unknown>;
21775
- producerCodexCredentialId: string | null;
21776
21767
  providerArtifactInvalidatedAt: Date | null;
21777
21768
  }>
21778
21769
  > {
21779
21770
  return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
21780
21771
  const rows = await scopedDb
21781
21772
  .select({
21773
+ id: schema.sessionHistoryItems.id,
21782
21774
  position: schema.sessionHistoryItems.position,
21783
21775
  item: schema.sessionHistoryItems.item,
21784
- producerCodexCredentialId: schema.sessionHistoryItems.producerCodexCredentialId,
21785
21776
  providerArtifactInvalidatedAt: schema.sessionHistoryItems.providerArtifactInvalidatedAt,
21786
21777
  })
21787
21778
  .from(schema.sessionHistoryItems)
@@ -21979,8 +21970,6 @@ export async function applyContextCompaction(
21979
21970
  replacementInputTokens: number;
21980
21971
  clearRequestedCompaction?: boolean;
21981
21972
  eventPayload?: Record<string, unknown>;
21982
- /** Tag inserted history rows with the Codex credential that produced them. */
21983
- producerCodexCredentialId?: string | null;
21984
21973
  },
21985
21974
  ): Promise<ApplyContextCompactionResult> {
21986
21975
  return await withRlsContext(
@@ -22030,9 +22019,6 @@ export async function applyContextCompaction(
22030
22019
  position: supersededFrom + index,
22031
22020
  item: sanitizeModelPayload(item),
22032
22021
  active: true,
22033
- ...(input.producerCodexCredentialId
22034
- ? { producerCodexCredentialId: input.producerCodexCredentialId }
22035
- : {}),
22036
22022
  })),
22037
22023
  );
22038
22024
  }
@@ -22045,9 +22031,6 @@ export async function applyContextCompaction(
22045
22031
  position: summaryPosition,
22046
22032
  item: sanitizeModelPayload(input.summaryItem),
22047
22033
  active: true,
22048
- ...(input.producerCodexCredentialId
22049
- ? { producerCodexCredentialId: input.producerCodexCredentialId }
22050
- : {}),
22051
22034
  });
22052
22035
  const insertedEvents = input.eventPayload
22053
22036
  ? await tx
@@ -34743,11 +34726,6 @@ export async function saveRunState(
34743
34726
  expectedAttemptId: string;
34744
34727
  serializedRunState: string;
34745
34728
  pendingApprovals: unknown[];
34746
- // The codex account freezing this state (the turn's resolved credential id),
34747
- // or null on a non-codex turn. Stamped so a resume on a DIFFERENT codex
34748
- // account can strip the blob's account-bound reasoning. Defaults null so
34749
- // every legacy caller (and the non-codex path) is byte-identical.
34750
- frozenCodexCredentialId?: string | null;
34751
34729
  },
34752
34730
  ): Promise<boolean> {
34753
34731
  return await withRlsContext(
@@ -34782,7 +34760,6 @@ export async function saveRunState(
34782
34760
  stateVersion: Number(maxVersion) + 1,
34783
34761
  serializedRunState: input.serializedRunState,
34784
34762
  pendingApprovals: input.pendingApprovals,
34785
- frozenCodexCredentialId: input.frozenCodexCredentialId ?? null,
34786
34763
  });
34787
34764
  return true;
34788
34765
  });
@@ -37492,7 +37469,6 @@ export async function claimSessionWorkForAttempt(
37492
37469
  turnId,
37493
37470
  position: Number(position),
37494
37471
  item: sanitizeModelPayload(delivered.historyItem),
37495
- producerCodexCredentialId: null,
37496
37472
  });
37497
37473
  };
37498
37474
 
@@ -38540,7 +38516,6 @@ export async function claimSessionWorkForAttempt(
38540
38516
  row.prompt,
38541
38517
  Array.isArray(row.resources) ? (row.resources as ResourceRef[]) : [],
38542
38518
  ),
38543
- producerCodexCredentialId: null,
38544
38519
  });
38545
38520
  const providerDelegatedTurn = isSessionRealtimeDelegationTurnMetadata(row.metadata);
38546
38521
  // Cross-session updates are already projected through
@@ -40176,7 +40151,6 @@ export type ApplySessionTurnSettlementInput = {
40176
40151
  runState?: {
40177
40152
  serializedRunState: string;
40178
40153
  pendingApprovals: unknown[];
40179
- frozenCodexCredentialId?: string | null;
40180
40154
  humanInputRequests?: Array<{
40181
40155
  id: string;
40182
40156
  toolCallId: string;
@@ -40405,7 +40379,6 @@ export async function applySessionTurnSettlement(
40405
40379
  stateVersion: Number(maxVersion) + 1,
40406
40380
  serializedRunState: input.runState.serializedRunState,
40407
40381
  pendingApprovals: input.runState.pendingApprovals,
40408
- frozenCodexCredentialId: input.runState.frozenCodexCredentialId ?? null,
40409
40382
  });
40410
40383
  if (humanInputRequests.length > 0) {
40411
40384
  for (const request of humanInputRequests) {
@@ -41390,7 +41363,8 @@ export type RequestSessionTurnRecoveryInput = {
41390
41363
  providerRecoveryCount?: number;
41391
41364
  fromStatuses?: SessionTurnStatus[];
41392
41365
  providerArtifactInvalidation?: {
41393
- codexCredentialId: string;
41366
+ historyItemIds: string[];
41367
+ runStateId?: string;
41394
41368
  reason: "encrypted_content_rejected";
41395
41369
  };
41396
41370
  };
@@ -41493,47 +41467,36 @@ export async function requestSessionTurnRecovery(
41493
41467
  }
41494
41468
  let providerArtifactsInvalidated = 0;
41495
41469
  if (input.providerArtifactInvalidation) {
41496
- const invalidatedHistory = await tx
41497
- .update(schema.sessionHistoryItems)
41498
- .set({
41499
- providerArtifactInvalidatedAt: now,
41500
- providerArtifactInvalidationReason: input.providerArtifactInvalidation.reason,
41501
- providerArtifactInvalidatedByAttemptId: input.attemptId,
41502
- })
41503
- .where(
41504
- and(
41505
- eq(schema.sessionHistoryItems.accountId, session.accountId),
41506
- eq(schema.sessionHistoryItems.workspaceId, workspaceId),
41507
- eq(schema.sessionHistoryItems.sessionId, input.sessionId),
41508
- eq(schema.sessionHistoryItems.active, true),
41509
- eq(
41510
- schema.sessionHistoryItems.producerCodexCredentialId,
41511
- input.providerArtifactInvalidation.codexCredentialId,
41512
- ),
41513
- isNull(schema.sessionHistoryItems.providerArtifactInvalidatedAt),
41514
- sql`${schema.sessionHistoryItems.item} ->> 'type' in ('reasoning', 'compaction')`,
41515
- ),
41516
- )
41517
- .returning({ id: schema.sessionHistoryItems.id });
41518
- const [latestRunState] = await tx
41519
- .select({ id: schema.agentRunStates.id })
41520
- .from(schema.agentRunStates)
41521
- .where(
41522
- and(
41523
- eq(schema.agentRunStates.accountId, session.accountId),
41524
- eq(schema.agentRunStates.workspaceId, workspaceId),
41525
- eq(schema.agentRunStates.sessionId, input.sessionId),
41526
- eq(schema.agentRunStates.turnId, input.turnId),
41527
- eq(
41528
- schema.agentRunStates.frozenCodexCredentialId,
41529
- input.providerArtifactInvalidation.codexCredentialId,
41530
- ),
41531
- isNull(schema.agentRunStates.providerArtifactInvalidatedAt),
41532
- ),
41533
- )
41534
- .orderBy(desc(schema.agentRunStates.stateVersion))
41535
- .limit(1);
41536
- const invalidatedRunState = latestRunState
41470
+ const historyItemIds = [...new Set(input.providerArtifactInvalidation.historyItemIds)];
41471
+ const invalidatedHistory =
41472
+ historyItemIds.length > 0
41473
+ ? await tx
41474
+ .update(schema.sessionHistoryItems)
41475
+ .set({
41476
+ providerArtifactInvalidatedAt: now,
41477
+ providerArtifactInvalidationReason: input.providerArtifactInvalidation.reason,
41478
+ providerArtifactInvalidatedByAttemptId: input.attemptId,
41479
+ })
41480
+ .where(
41481
+ and(
41482
+ eq(schema.sessionHistoryItems.accountId, session.accountId),
41483
+ eq(schema.sessionHistoryItems.workspaceId, workspaceId),
41484
+ eq(schema.sessionHistoryItems.sessionId, input.sessionId),
41485
+ eq(schema.sessionHistoryItems.active, true),
41486
+ inArray(schema.sessionHistoryItems.id, historyItemIds),
41487
+ isNull(schema.sessionHistoryItems.providerArtifactInvalidatedAt),
41488
+ sql`${schema.sessionHistoryItems.item} ->> 'type' in ('reasoning', 'compaction')`,
41489
+ sql`(
41490
+ nullif(${schema.sessionHistoryItems.item} ->> 'encrypted_content', '') is not null
41491
+ or nullif(${schema.sessionHistoryItems.item} ->> 'encryptedContent', '') is not null
41492
+ or nullif(${schema.sessionHistoryItems.item} -> 'providerData' ->> 'encrypted_content', '') is not null
41493
+ or nullif(${schema.sessionHistoryItems.item} -> 'providerData' ->> 'encryptedContent', '') is not null
41494
+ )`,
41495
+ ),
41496
+ )
41497
+ .returning({ id: schema.sessionHistoryItems.id })
41498
+ : [];
41499
+ const invalidatedRunState = input.providerArtifactInvalidation.runStateId
41537
41500
  ? await tx
41538
41501
  .update(schema.agentRunStates)
41539
41502
  .set({
@@ -41543,8 +41506,20 @@ export async function requestSessionTurnRecovery(
41543
41506
  })
41544
41507
  .where(
41545
41508
  and(
41546
- eq(schema.agentRunStates.id, latestRunState.id),
41509
+ eq(schema.agentRunStates.accountId, session.accountId),
41510
+ eq(schema.agentRunStates.workspaceId, workspaceId),
41511
+ eq(schema.agentRunStates.sessionId, input.sessionId),
41512
+ eq(schema.agentRunStates.turnId, input.turnId),
41513
+ eq(schema.agentRunStates.id, input.providerArtifactInvalidation.runStateId),
41547
41514
  isNull(schema.agentRunStates.providerArtifactInvalidatedAt),
41515
+ sql`${schema.agentRunStates.stateVersion} = (
41516
+ select max(latest.state_version)
41517
+ from agent_run_states latest
41518
+ where latest.account_id = ${session.accountId}
41519
+ and latest.workspace_id = ${workspaceId}
41520
+ and latest.session_id = ${input.sessionId}
41521
+ and latest.turn_id = ${input.turnId}
41522
+ )`,
41548
41523
  ),
41549
41524
  )
41550
41525
  .returning({ id: schema.agentRunStates.id })
@@ -44809,6 +44784,7 @@ function mapSocialConnection(
44809
44784
  accountHandle: row.accountHandle,
44810
44785
  accountName: row.accountName,
44811
44786
  externalAccountId: row.externalAccountId,
44787
+ ownership: row.subjectId ? "personal" : "workspace",
44812
44788
  status: row.status as SocialConnectionStatus,
44813
44789
  scopes: row.scopes,
44814
44790
  credentialRef: row.credentialRef,
@@ -44819,6 +44795,25 @@ function mapSocialConnection(
44819
44795
  };
44820
44796
  }
44821
44797
 
44798
+ function socialConnectionSubjectVisibility(subjectId?: string | null): SQL {
44799
+ return subjectId
44800
+ ? or(
44801
+ isNull(schema.socialConnections.subjectId),
44802
+ eq(schema.socialConnections.subjectId, subjectId),
44803
+ )!
44804
+ : isNull(schema.socialConnections.subjectId);
44805
+ }
44806
+ async function withSocialConnectionSubjectRls<T>(
44807
+ db: Database,
44808
+ workspaceId: string,
44809
+ subjectId: string | null | undefined,
44810
+ fn: (db: Database) => Promise<T>,
44811
+ ): Promise<T> {
44812
+ return subjectId
44813
+ ? await withWorkspaceSubjectRls(db, workspaceId, subjectId, fn)
44814
+ : await withWorkspaceRls(db, workspaceId, fn);
44815
+ }
44816
+
44822
44817
  function mapApiKey(row: typeof schema.apiKeys.$inferSelect): ApiKey {
44823
44818
  return {
44824
44819
  id: row.id,
@@ -45027,11 +45022,118 @@ function shortHash(value: string): string {
45027
45022
  return (hash >>> 0).toString(36).padStart(7, "0").slice(0, 7);
45028
45023
  }
45029
45024
 
45030
- // Shared, refreshing, id-addressed Codex token resolver + the per-account usage
45031
- // wrapper (P2). Placed at the END so every accessor it orchestrates
45032
- // (loadCodexCredentialForRun / recordCodexTokenRefresh / setCodexCredentialStatus /
45033
- // recordCodexAccountUsage) is already initialized when its default-deps bag
45034
- // evaluates under the index↔resolver module cycle.
45035
- export * from "./codex-token-resolver";
45036
- export * from "./connection-token-resolver";
45025
+ // The resolver modules are cycle-free orchestration leaves. The root barrel is
45026
+ // the composition point that supplies their existing persistence accessors while
45027
+ // retaining the historical public builder/fetcher signatures.
45028
+ function codexAuthDeps(): CodexAuthDeps {
45029
+ return {
45030
+ loadCredential: loadCodexCredentialForRun,
45031
+ recordRefresh: recordCodexTokenRefresh,
45032
+ setStatus: setCodexCredentialStatus,
45033
+ refresh: refreshCodexToken,
45034
+ encrypt: encryptEnvironmentValue,
45035
+ keyBytes: environmentsEncryptionKeyBytes,
45036
+ withRefreshLock: withCodexCredentialRefreshLock,
45037
+ recordUsage: recordCodexAccountUsage,
45038
+ };
45039
+ }
45040
+
45041
+ function connectionBrokerDeps(): ConnectionBrokerDeps {
45042
+ return {
45043
+ loadCredential: loadConnectionCredentialForBroker,
45044
+ recordRefresh: recordConnectionTokenRefresh,
45045
+ setStatus: setConnectionStatus,
45046
+ recordUsed: recordConnectionUsed,
45047
+ refresh: refreshOAuthConnectionCredentialCore,
45048
+ encrypt: encryptEnvironmentValue,
45049
+ keyBytes: environmentsEncryptionKeyBytes,
45050
+ now: () => new Date(),
45051
+ };
45052
+ }
45053
+
45054
+ export function buildCodexTokenResolver(
45055
+ db: Database,
45056
+ settings: Settings,
45057
+ workspaceId: string,
45058
+ credentialId: string,
45059
+ deps: CodexAuthDeps = codexAuthDeps(),
45060
+ ): ReturnType<typeof buildCodexTokenResolverCore> {
45061
+ return buildCodexTokenResolverCore(db, settings, workspaceId, credentialId, deps);
45062
+ }
45063
+
45064
+ export async function fetchCodexUsageForAccount(
45065
+ db: Database,
45066
+ settings: Settings,
45067
+ workspaceId: string,
45068
+ credentialId: string,
45069
+ fetchImpl: CodexFetch = fetch,
45070
+ ): ReturnType<typeof fetchCodexUsageForAccountCore> {
45071
+ return await fetchCodexUsageForAccountCore(
45072
+ db,
45073
+ settings,
45074
+ workspaceId,
45075
+ credentialId,
45076
+ codexAuthDeps(),
45077
+ fetchImpl,
45078
+ );
45079
+ }
45080
+
45081
+ export async function fetchCodexRateLimitResetCreditsForAccount(
45082
+ db: Database,
45083
+ settings: Settings,
45084
+ workspaceId: string,
45085
+ credentialId: string,
45086
+ fetchImpl: CodexFetch = fetch,
45087
+ ): ReturnType<typeof fetchCodexRateLimitResetCreditsForAccountCore> {
45088
+ return await fetchCodexRateLimitResetCreditsForAccountCore(
45089
+ db,
45090
+ settings,
45091
+ workspaceId,
45092
+ credentialId,
45093
+ codexAuthDeps(),
45094
+ fetchImpl,
45095
+ );
45096
+ }
45097
+
45098
+ export function buildConnectionTokenResolver(
45099
+ db: Database,
45100
+ settings: Settings,
45101
+ deps: ConnectionBrokerDeps = connectionBrokerDeps(),
45102
+ options: ConnectionTokenResolverOptions = {},
45103
+ ): ReturnType<typeof buildConnectionTokenResolverCore> {
45104
+ return buildConnectionTokenResolverCore(db, settings, deps, options);
45105
+ }
45106
+
45107
+ export {
45108
+ withCodexTokenDeadline,
45109
+ type CodexAccountUsageSnapshot,
45110
+ type CodexAuthDeps,
45111
+ type CodexCredentialForRun,
45112
+ type CodexCredentialTokens,
45113
+ type CodexRateLimitResetCreditsAccountResult,
45114
+ type CodexTokenDeadlineClock,
45115
+ type CodexTokenDeadlineOptions,
45116
+ } from "./codex-token-resolver";
45117
+
45118
+ export {
45119
+ buildHostConnectionTokenResolver,
45120
+ ConnectionRefreshHttpError,
45121
+ HostMcpCredentialBindingError,
45122
+ HostMcpCredentialScopeError,
45123
+ isPrivateAddress,
45124
+ normalizeBearerScheme,
45125
+ refreshOAuthConnectionCredential,
45126
+ type ConnectionBrokerDeps,
45127
+ type ConnectionCredentialForBroker,
45128
+ type ConnectionCredentialLookupInput,
45129
+ type ConnectionStatusGuard,
45130
+ type ConnectionTokenRefreshInput,
45131
+ type ConnectionTokenResolverOptions,
45132
+ type HostMcpCredentialResolverContext,
45133
+ type PermanentConnectionRefreshFailure,
45134
+ type RefreshTransportOptions,
45135
+ type ResolveConnectionCredentialInput,
45136
+ type ResolveConnectionCredentialResult,
45137
+ } from "./connection-token-resolver";
45138
+
45037
45139
  export * from "./workspace-artifacts";