@opengeni/db 0.12.6 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -214,6 +214,7 @@ export { sql as dbSql } from "drizzle-orm";
214
214
  export * from "./session-control";
215
215
  export * from "./session-queue-commands";
216
216
  export * from "./new-session-drafts";
217
+ export * from "./workspace-instruction-policies";
217
218
  export { interruptedToolCallResult } from "./session-tool-call-settlement";
218
219
  export { decryptEnvironmentValue, encryptEnvironmentValue } from "./environment-crypto";
219
220
  export {
@@ -3144,6 +3145,8 @@ export type CreateConnectionInput = {
3144
3145
  credentialEncrypted: string;
3145
3146
  grantedScopes?: string[];
3146
3147
  expiresAt?: Date | null;
3148
+ verifiedInstallAt?: Date | null;
3149
+ verifiedInstallVersion?: number | null;
3147
3150
  metadata?: Record<string, unknown>;
3148
3151
  createdBySubjectId?: string | null;
3149
3152
  updatedBySubjectId?: string | null;
@@ -3161,10 +3164,40 @@ export type UpdateConnectionInput = {
3161
3164
  credentialEncrypted?: string;
3162
3165
  grantedScopes?: string[];
3163
3166
  expiresAt?: Date | null;
3167
+ verifiedInstallAt?: Date | null;
3168
+ verifiedInstallVersion?: number | null;
3164
3169
  metadata?: Record<string, unknown>;
3165
3170
  updatedBySubjectId?: string | null;
3166
3171
  };
3167
3172
 
3173
+ /** Server-owned verification facts; public schemas expose them read-only and nullable. */
3174
+ export type ConnectionMetadataWithVerification = ConnectionMetadata & {
3175
+ verifiedInstallAt: string | null;
3176
+ verifiedInstallVersion: number | null;
3177
+ };
3178
+
3179
+ export type SlackBotPostOperation = {
3180
+ id: string;
3181
+ accountId: string;
3182
+ workspaceId: string;
3183
+ connectionId: string;
3184
+ operationId: string;
3185
+ clientMessageId: string;
3186
+ targetKind: "channel" | "user";
3187
+ targetId: string;
3188
+ requestDigest: string;
3189
+ status: "provider_started" | "completed";
3190
+ claimHolderId: string | null;
3191
+ claimExpiresAt: Date | null;
3192
+ attemptCount: number;
3193
+ lastFailureCode: string | null;
3194
+ slackChannelId: string | null;
3195
+ slackMessageTimestamp: string | null;
3196
+ completedAt: Date | null;
3197
+ createdAt: Date;
3198
+ updatedAt: Date;
3199
+ };
3200
+
3168
3201
  export type ConnectionCredentialForBroker = {
3169
3202
  id: string;
3170
3203
  accountId: string;
@@ -3295,6 +3328,7 @@ export type RegistryCapabilityCatalogItemInput = {
3295
3328
  importBatchId: string;
3296
3329
  scopesHint?: string[];
3297
3330
  homepageUrl?: string | null;
3331
+ installUrl?: string | null;
3298
3332
  tags?: string[];
3299
3333
  metadata?: Record<string, unknown>;
3300
3334
  };
@@ -4163,7 +4197,7 @@ export async function upsertRegistryCapabilityCatalogItem(
4163
4197
  tags: input.tags ?? ["mcp", "integration", input.tier],
4164
4198
  homepageUrl: input.homepageUrl ?? `https://${input.providerDomain}`,
4165
4199
  endpointUrl: input.mcpUrl,
4166
- installUrl: input.homepageUrl ?? `https://${input.providerDomain}`,
4200
+ installUrl: input.installUrl ?? input.homepageUrl ?? `https://${input.providerDomain}`,
4167
4201
  authModel: input.authKind === "none" ? null : "credential_ref",
4168
4202
  providerDomain: input.providerDomain,
4169
4203
  surfaceType: "mcp",
@@ -4723,6 +4757,8 @@ const connectionMetadataColumns = {
4723
4757
  lastUsedAt: schema.connections.lastUsedAt,
4724
4758
  lastError: schema.connections.lastError,
4725
4759
  version: schema.connections.version,
4760
+ verifiedInstallAt: schema.connections.verifiedInstallAt,
4761
+ verifiedInstallVersion: schema.connections.verifiedInstallVersion,
4726
4762
  metadata: schema.connections.metadata,
4727
4763
  createdBySubjectId: schema.connections.createdBySubjectId,
4728
4764
  updatedBySubjectId: schema.connections.updatedBySubjectId,
@@ -4739,7 +4775,7 @@ function connectionSubjectVisibility(subjectId?: string | null): SQL {
4739
4775
  export async function createConnection(
4740
4776
  db: Database,
4741
4777
  input: CreateConnectionInput,
4742
- ): Promise<ConnectionMetadata> {
4778
+ ): Promise<ConnectionMetadataWithVerification> {
4743
4779
  return await withRlsContext(
4744
4780
  db,
4745
4781
  { accountId: input.accountId, workspaceId: input.workspaceId },
@@ -4756,6 +4792,8 @@ export async function createConnection(
4756
4792
  credentialEncrypted: input.credentialEncrypted,
4757
4793
  grantedScopes: input.grantedScopes ?? [],
4758
4794
  expiresAt: input.expiresAt ?? null,
4795
+ verifiedInstallAt: input.verifiedInstallAt ?? null,
4796
+ verifiedInstallVersion: input.verifiedInstallVersion ?? null,
4759
4797
  metadata: input.metadata ?? {},
4760
4798
  createdBySubjectId: input.createdBySubjectId ?? null,
4761
4799
  updatedBySubjectId: input.updatedBySubjectId ?? input.createdBySubjectId ?? null,
@@ -4773,7 +4811,7 @@ export async function listConnectionsMetadata(
4773
4811
  db: Database,
4774
4812
  workspaceId: string,
4775
4813
  subjectId?: string | null,
4776
- ): Promise<ConnectionMetadata[]> {
4814
+ ): Promise<ConnectionMetadataWithVerification[]> {
4777
4815
  return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
4778
4816
  const rows = await scopedDb
4779
4817
  .select(connectionMetadataColumns)
@@ -4794,7 +4832,7 @@ export async function getConnectionMetadata(
4794
4832
  workspaceId: string,
4795
4833
  connectionId: string,
4796
4834
  subjectId?: string | null,
4797
- ): Promise<ConnectionMetadata | null> {
4835
+ ): Promise<ConnectionMetadataWithVerification | null> {
4798
4836
  return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
4799
4837
  const [row] = await scopedDb
4800
4838
  .select(connectionMetadataColumns)
@@ -4814,7 +4852,7 @@ export async function getConnectionMetadata(
4814
4852
  export async function updateConnection(
4815
4853
  db: Database,
4816
4854
  input: UpdateConnectionInput,
4817
- ): Promise<ConnectionMetadata | null> {
4855
+ ): Promise<ConnectionMetadataWithVerification | null> {
4818
4856
  return await withWorkspaceRls(db, input.workspaceId, async (scopedDb) => {
4819
4857
  const set = {
4820
4858
  updatedAt: new Date(),
@@ -4831,6 +4869,12 @@ export async function updateConnection(
4831
4869
  : {}),
4832
4870
  ...(input.grantedScopes !== undefined ? { grantedScopes: input.grantedScopes } : {}),
4833
4871
  ...(input.expiresAt !== undefined ? { expiresAt: input.expiresAt } : {}),
4872
+ ...(input.verifiedInstallAt !== undefined
4873
+ ? { verifiedInstallAt: input.verifiedInstallAt }
4874
+ : {}),
4875
+ ...(input.verifiedInstallVersion !== undefined
4876
+ ? { verifiedInstallVersion: input.verifiedInstallVersion }
4877
+ : {}),
4834
4878
  ...(input.metadata !== undefined ? { metadata: input.metadata } : {}),
4835
4879
  ...(input.updatedBySubjectId !== undefined
4836
4880
  ? { updatedBySubjectId: input.updatedBySubjectId }
@@ -4859,7 +4903,7 @@ export async function revokeConnection(
4859
4903
  workspaceId: string,
4860
4904
  connectionId: string,
4861
4905
  updatedBySubjectId?: string | null,
4862
- ): Promise<ConnectionMetadata | null> {
4906
+ ): Promise<ConnectionMetadataWithVerification | null> {
4863
4907
  return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
4864
4908
  const [row] = await scopedDb
4865
4909
  .update(schema.connections)
@@ -4868,6 +4912,13 @@ export async function revokeConnection(
4868
4912
  // The version bump invalidates any in-flight refresh's (id, version) CAS,
4869
4913
  // so a racing refresh cannot commit and flip the row back to active.
4870
4914
  version: sql`${schema.connections.version} + 1`,
4915
+ // Status-only revocation does not replace the verified credential or bot
4916
+ // identity. Carry the marker to the same new CAS version so the dedicated
4917
+ // reinstall path can still recognize (but not use) the inactive row.
4918
+ verifiedInstallVersion: sql`case
4919
+ when ${schema.connections.verifiedInstallAt} is null then null
4920
+ else ${schema.connections.version} + 1
4921
+ end`,
4871
4922
  updatedBySubjectId: updatedBySubjectId ?? null,
4872
4923
  updatedAt: new Date(),
4873
4924
  })
@@ -4886,6 +4937,272 @@ export async function revokeConnection(
4886
4937
  });
4887
4938
  }
4888
4939
 
4940
+ export type ClaimSlackBotPostOperationResult =
4941
+ | { kind: "claimed" | "in_progress" | "completed"; operation: SlackBotPostOperation }
4942
+ | { kind: "conflict" | "connection_not_found" };
4943
+
4944
+ /**
4945
+ * Claims one durable Slack post identity. The insert occurs before any provider
4946
+ * call; retries retain the original client_msg_id and immutable request digest.
4947
+ * A live claim suppresses concurrent sends, while a released/expired claim can
4948
+ * be reclaimed after response loss or process death.
4949
+ */
4950
+ export async function claimSlackBotPostOperation(
4951
+ db: Database,
4952
+ input: {
4953
+ accountId: string;
4954
+ workspaceId: string;
4955
+ connectionId: string;
4956
+ operationId: string;
4957
+ targetKind: "channel" | "user";
4958
+ targetId: string;
4959
+ requestDigest: string;
4960
+ claimHolderId: string;
4961
+ claimLeaseMs: number;
4962
+ },
4963
+ ): Promise<ClaimSlackBotPostOperationResult> {
4964
+ const claimLeaseMs = Math.max(1, Math.min(Math.trunc(input.claimLeaseMs), 120_000));
4965
+ return await withRlsContext(
4966
+ db,
4967
+ { accountId: input.accountId, workspaceId: input.workspaceId },
4968
+ async (scopedDb) =>
4969
+ await scopedDb.transaction(async (txRaw) => {
4970
+ const tx = txRaw as unknown as Database;
4971
+ const [connection] = await tx
4972
+ .select({ id: schema.connections.id })
4973
+ .from(schema.connections)
4974
+ .where(
4975
+ and(
4976
+ eq(schema.connections.accountId, input.accountId),
4977
+ eq(schema.connections.workspaceId, input.workspaceId),
4978
+ eq(schema.connections.id, input.connectionId),
4979
+ ),
4980
+ )
4981
+ .for("share")
4982
+ .limit(1);
4983
+ if (!connection) return { kind: "connection_not_found" } as const;
4984
+
4985
+ const [created] = await tx
4986
+ .insert(schema.slackBotPostOperations)
4987
+ .values({
4988
+ accountId: input.accountId,
4989
+ workspaceId: input.workspaceId,
4990
+ connectionId: input.connectionId,
4991
+ operationId: input.operationId,
4992
+ clientMessageId: input.operationId,
4993
+ targetKind: input.targetKind,
4994
+ targetId: input.targetId,
4995
+ requestDigest: input.requestDigest,
4996
+ status: "provider_started",
4997
+ claimHolderId: input.claimHolderId,
4998
+ claimExpiresAt: sql`now() + (${claimLeaseMs} * interval '1 millisecond')`,
4999
+ attemptCount: 1,
5000
+ })
5001
+ .onConflictDoNothing({
5002
+ target: [
5003
+ schema.slackBotPostOperations.workspaceId,
5004
+ schema.slackBotPostOperations.connectionId,
5005
+ schema.slackBotPostOperations.operationId,
5006
+ ],
5007
+ })
5008
+ .returning();
5009
+ if (created) {
5010
+ return { kind: "claimed", operation: mapSlackBotPostOperation(created) } as const;
5011
+ }
5012
+
5013
+ const [existing] = await tx
5014
+ .select()
5015
+ .from(schema.slackBotPostOperations)
5016
+ .where(
5017
+ and(
5018
+ eq(schema.slackBotPostOperations.workspaceId, input.workspaceId),
5019
+ eq(schema.slackBotPostOperations.connectionId, input.connectionId),
5020
+ eq(schema.slackBotPostOperations.operationId, input.operationId),
5021
+ ),
5022
+ )
5023
+ .for("update")
5024
+ .limit(1);
5025
+ if (!existing) throw new Error("Slack post operation disappeared after conflict");
5026
+ if (
5027
+ existing.accountId !== input.accountId ||
5028
+ existing.targetKind !== input.targetKind ||
5029
+ existing.targetId !== input.targetId ||
5030
+ existing.requestDigest !== input.requestDigest ||
5031
+ existing.clientMessageId !== input.operationId
5032
+ ) {
5033
+ return { kind: "conflict" } as const;
5034
+ }
5035
+ if (existing.status === "completed") {
5036
+ return { kind: "completed", operation: mapSlackBotPostOperation(existing) } as const;
5037
+ }
5038
+
5039
+ const [reclaimed] = await tx
5040
+ .update(schema.slackBotPostOperations)
5041
+ .set({
5042
+ claimHolderId: input.claimHolderId,
5043
+ claimExpiresAt: sql`now() + (${claimLeaseMs} * interval '1 millisecond')`,
5044
+ attemptCount: sql`${schema.slackBotPostOperations.attemptCount} + 1`,
5045
+ lastFailureCode: null,
5046
+ updatedAt: sql`now()`,
5047
+ })
5048
+ .where(
5049
+ and(
5050
+ eq(schema.slackBotPostOperations.id, existing.id),
5051
+ or(
5052
+ isNull(schema.slackBotPostOperations.claimHolderId),
5053
+ lte(schema.slackBotPostOperations.claimExpiresAt, sql`now()`),
5054
+ ),
5055
+ ),
5056
+ )
5057
+ .returning();
5058
+ return reclaimed
5059
+ ? ({ kind: "claimed", operation: mapSlackBotPostOperation(reclaimed) } as const)
5060
+ : ({ kind: "in_progress", operation: mapSlackBotPostOperation(existing) } as const);
5061
+ }),
5062
+ );
5063
+ }
5064
+
5065
+ export async function releaseSlackBotPostOperationClaim(
5066
+ db: Database,
5067
+ input: {
5068
+ accountId: string;
5069
+ workspaceId: string;
5070
+ connectionId: string;
5071
+ operationId: string;
5072
+ claimHolderId: string;
5073
+ failureCode: string;
5074
+ },
5075
+ ): Promise<boolean> {
5076
+ return await withRlsContext(
5077
+ db,
5078
+ { accountId: input.accountId, workspaceId: input.workspaceId },
5079
+ async (scopedDb) => {
5080
+ const rows = await scopedDb
5081
+ .update(schema.slackBotPostOperations)
5082
+ .set({
5083
+ claimHolderId: null,
5084
+ claimExpiresAt: null,
5085
+ lastFailureCode: input.failureCode.slice(0, 128),
5086
+ updatedAt: sql`now()`,
5087
+ })
5088
+ .where(
5089
+ and(
5090
+ eq(schema.slackBotPostOperations.workspaceId, input.workspaceId),
5091
+ eq(schema.slackBotPostOperations.connectionId, input.connectionId),
5092
+ eq(schema.slackBotPostOperations.operationId, input.operationId),
5093
+ eq(schema.slackBotPostOperations.status, "provider_started"),
5094
+ eq(schema.slackBotPostOperations.claimHolderId, input.claimHolderId),
5095
+ ),
5096
+ )
5097
+ .returning({ id: schema.slackBotPostOperations.id });
5098
+ return rows.length === 1;
5099
+ },
5100
+ );
5101
+ }
5102
+
5103
+ export type CompleteSlackBotPostOperationResult =
5104
+ | { kind: "completed"; operation: SlackBotPostOperation; newlyCompleted: boolean }
5105
+ | { kind: "not_found" | "not_owned" };
5106
+
5107
+ /** Completion and the single success audit receipt commit atomically. */
5108
+ export async function completeSlackBotPostOperation(
5109
+ db: Database,
5110
+ input: {
5111
+ accountId: string;
5112
+ workspaceId: string;
5113
+ connectionId: string;
5114
+ operationId: string;
5115
+ claimHolderId: string;
5116
+ slackChannelId: string;
5117
+ slackMessageTimestamp: string;
5118
+ subjectId?: string | null;
5119
+ auditMetadata: Record<string, unknown>;
5120
+ },
5121
+ ): Promise<CompleteSlackBotPostOperationResult> {
5122
+ return await withRlsContext(
5123
+ db,
5124
+ { accountId: input.accountId, workspaceId: input.workspaceId },
5125
+ async (scopedDb) =>
5126
+ await scopedDb.transaction(async (txRaw) => {
5127
+ const tx = txRaw as unknown as Database;
5128
+ const [current] = await tx
5129
+ .select()
5130
+ .from(schema.slackBotPostOperations)
5131
+ .where(
5132
+ and(
5133
+ eq(schema.slackBotPostOperations.workspaceId, input.workspaceId),
5134
+ eq(schema.slackBotPostOperations.connectionId, input.connectionId),
5135
+ eq(schema.slackBotPostOperations.operationId, input.operationId),
5136
+ ),
5137
+ )
5138
+ .for("update")
5139
+ .limit(1);
5140
+ if (!current) return { kind: "not_found" } as const;
5141
+ if (current.status === "completed") {
5142
+ return {
5143
+ kind: "completed",
5144
+ operation: mapSlackBotPostOperation(current),
5145
+ newlyCompleted: false,
5146
+ } as const;
5147
+ }
5148
+ if (current.claimHolderId !== input.claimHolderId) {
5149
+ return { kind: "not_owned" } as const;
5150
+ }
5151
+ const [completed] = await tx
5152
+ .update(schema.slackBotPostOperations)
5153
+ .set({
5154
+ status: "completed",
5155
+ claimHolderId: null,
5156
+ claimExpiresAt: null,
5157
+ lastFailureCode: null,
5158
+ slackChannelId: input.slackChannelId,
5159
+ slackMessageTimestamp: input.slackMessageTimestamp,
5160
+ completedAt: sql`now()`,
5161
+ updatedAt: sql`now()`,
5162
+ })
5163
+ .where(eq(schema.slackBotPostOperations.id, current.id))
5164
+ .returning();
5165
+ if (!completed) throw new Error("Slack post completion returned no row");
5166
+ await tx.insert(schema.auditEvents).values({
5167
+ accountId: input.accountId,
5168
+ workspaceId: input.workspaceId,
5169
+ subjectId: input.subjectId ?? null,
5170
+ action: "slack_bot.message.post",
5171
+ targetType: "connection",
5172
+ targetId: input.connectionId,
5173
+ metadata: input.auditMetadata,
5174
+ });
5175
+ return {
5176
+ kind: "completed",
5177
+ operation: mapSlackBotPostOperation(completed),
5178
+ newlyCompleted: true,
5179
+ } as const;
5180
+ }),
5181
+ );
5182
+ }
5183
+
5184
+ export async function getSlackBotPostOperation(
5185
+ db: Database,
5186
+ workspaceId: string,
5187
+ connectionId: string,
5188
+ operationId: string,
5189
+ ): Promise<SlackBotPostOperation | null> {
5190
+ return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
5191
+ const [row] = await scopedDb
5192
+ .select()
5193
+ .from(schema.slackBotPostOperations)
5194
+ .where(
5195
+ and(
5196
+ eq(schema.slackBotPostOperations.workspaceId, workspaceId),
5197
+ eq(schema.slackBotPostOperations.connectionId, connectionId),
5198
+ eq(schema.slackBotPostOperations.operationId, operationId),
5199
+ ),
5200
+ )
5201
+ .limit(1);
5202
+ return row ? mapSlackBotPostOperation(row) : null;
5203
+ });
5204
+ }
5205
+
4889
5206
  export async function loadConnectionCredentialForBroker(
4890
5207
  db: Database,
4891
5208
  settings: Settings,
@@ -5019,6 +5336,10 @@ export async function setConnectionStatus(
5019
5336
  status,
5020
5337
  lastError,
5021
5338
  version: sql`${schema.connections.version} + 1`,
5339
+ verifiedInstallVersion: sql`case
5340
+ when ${schema.connections.verifiedInstallAt} is null then null
5341
+ else ${schema.connections.version} + 1
5342
+ end`,
5022
5343
  updatedAt: new Date(),
5023
5344
  })
5024
5345
  .where(
@@ -35402,12 +35723,14 @@ function mapConnectionMetadata(row: {
35402
35723
  lastUsedAt: Date | null;
35403
35724
  lastError: string | null;
35404
35725
  version: number;
35726
+ verifiedInstallAt: Date | null;
35727
+ verifiedInstallVersion: number | null;
35405
35728
  metadata: Record<string, unknown>;
35406
35729
  createdBySubjectId: string | null;
35407
35730
  updatedBySubjectId: string | null;
35408
35731
  createdAt: Date;
35409
35732
  updatedAt: Date;
35410
- }): ConnectionMetadata {
35733
+ }): ConnectionMetadataWithVerification {
35411
35734
  return {
35412
35735
  id: row.id,
35413
35736
  accountId: row.accountId,
@@ -35422,6 +35745,8 @@ function mapConnectionMetadata(row: {
35422
35745
  lastUsedAt: row.lastUsedAt?.toISOString() ?? null,
35423
35746
  lastError: row.lastError,
35424
35747
  version: row.version,
35748
+ verifiedInstallAt: row.verifiedInstallAt?.toISOString() ?? null,
35749
+ verifiedInstallVersion: row.verifiedInstallVersion,
35425
35750
  metadata: row.metadata,
35426
35751
  createdBySubjectId: row.createdBySubjectId,
35427
35752
  updatedBySubjectId: row.updatedBySubjectId,
@@ -35430,6 +35755,32 @@ function mapConnectionMetadata(row: {
35430
35755
  };
35431
35756
  }
35432
35757
 
35758
+ function mapSlackBotPostOperation(
35759
+ row: typeof schema.slackBotPostOperations.$inferSelect,
35760
+ ): SlackBotPostOperation {
35761
+ return {
35762
+ id: row.id,
35763
+ accountId: row.accountId,
35764
+ workspaceId: row.workspaceId,
35765
+ connectionId: row.connectionId,
35766
+ operationId: row.operationId,
35767
+ clientMessageId: row.clientMessageId,
35768
+ targetKind: row.targetKind,
35769
+ targetId: row.targetId,
35770
+ requestDigest: row.requestDigest,
35771
+ status: row.status,
35772
+ claimHolderId: row.claimHolderId,
35773
+ claimExpiresAt: row.claimExpiresAt,
35774
+ attemptCount: row.attemptCount,
35775
+ lastFailureCode: row.lastFailureCode,
35776
+ slackChannelId: row.slackChannelId,
35777
+ slackMessageTimestamp: row.slackMessageTimestamp,
35778
+ completedAt: row.completedAt,
35779
+ createdAt: row.createdAt,
35780
+ updatedAt: row.updatedAt,
35781
+ };
35782
+ }
35783
+
35433
35784
  function mapKnowledgeMemory(row: typeof schema.knowledgeMemories.$inferSelect): KnowledgeMemory {
35434
35785
  return {
35435
35786
  id: row.id,
@@ -73,12 +73,16 @@ export const FORCE_RLS_TABLES = [
73
73
  "session_turns",
74
74
  "session_workflow_wake_outbox",
75
75
  "sessions",
76
+ "slack_bot_post_operations",
76
77
  "social_connections",
77
78
  "social_posts",
78
79
  "usage_events",
79
80
  "workspace_captures",
80
81
  "workspace_control_events",
81
82
  "workspace_inference_controls",
83
+ "workspace_instruction_policy_activation_events",
84
+ "workspace_instruction_policy_heads",
85
+ "workspace_instruction_policy_revisions",
82
86
  "workspace_model_policies",
83
87
  "workspace_packs",
84
88
  "workspace_session_activity_revisions",
@@ -178,6 +182,7 @@ export const RUNTIME_FULL_DML_TABLES = [
178
182
  "session_turns",
179
183
  "session_workflow_wake_outbox",
180
184
  "sessions",
185
+ "slack_bot_post_operations",
181
186
  "social_connections",
182
187
  "social_posts",
183
188
  "stripe_webhook_events",
@@ -185,6 +190,7 @@ export const RUNTIME_FULL_DML_TABLES = [
185
190
  "workspace_captures",
186
191
  "workspace_control_events",
187
192
  "workspace_inference_controls",
193
+ "workspace_instruction_policy_heads",
188
194
  "workspace_memberships",
189
195
  "workspace_model_policies",
190
196
  "workspace_packs",
@@ -197,8 +203,12 @@ export const RUNTIME_FULL_DML_TABLES = [
197
203
  /** Configuration is deployment-global and intentionally read-only at runtime. */
198
204
  export const RUNTIME_READ_ONLY_TABLES = ["nested_agent_depth_configuration"] as const;
199
205
 
200
- /** Spawn-denial evidence is append-only to the runtime, but remains queryable. */
201
- export const RUNTIME_READ_INSERT_TABLES = ["session_spawn_denials"] as const;
206
+ /** Append-only evidence/revision tables are insertable and queryable, never mutable. */
207
+ export const RUNTIME_READ_INSERT_TABLES = [
208
+ "session_spawn_denials",
209
+ "workspace_instruction_policy_activation_events",
210
+ "workspace_instruction_policy_revisions",
211
+ ] as const;
202
212
 
203
213
  /**
204
214
  * These FORCE-RLS tables are owned by security-definer host-export routines.
package/src/schema.ts CHANGED
@@ -485,6 +485,12 @@ export const connections = pgTable(
485
485
  lastUsedAt: timestamp("last_used_at", { withTimezone: true }),
486
486
  lastError: text("last_error"),
487
487
  version: integer("version").notNull().default(1),
488
+ // Server-owned proof that the dedicated install route verified the exact
489
+ // credential at this connection version. Generic/legacy writers cannot set
490
+ // these columns, and migration 0131 clears them when protected fields change
491
+ // without a fresh verification in the same statement.
492
+ verifiedInstallAt: timestamp("verified_install_at", { withTimezone: true }),
493
+ verifiedInstallVersion: integer("verified_install_version"),
488
494
  metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
489
495
  createdBySubjectId: text("created_by_subject_id"),
490
496
  updatedBySubjectId: text("updated_by_subject_id"),
@@ -510,6 +516,85 @@ export const connections = pgTable(
510
516
  }),
511
517
  );
512
518
 
519
+ // Durable provider-operation identity for OpenGeni Slack bot posts. The
520
+ // caller-supplied operation UUID is also Slack's client_msg_id; a bounded claim
521
+ // serializes live attempts while an expired/released claim can safely retry the
522
+ // same provider identity after response loss or process death.
523
+ export const slackBotPostOperations = pgTable(
524
+ "slack_bot_post_operations",
525
+ {
526
+ id: uuid("id").primaryKey().defaultRandom(),
527
+ accountId: uuid("account_id")
528
+ .notNull()
529
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
530
+ workspaceId: uuid("workspace_id")
531
+ .notNull()
532
+ .references(() => workspaces.id, { onDelete: "cascade" }),
533
+ connectionId: uuid("connection_id")
534
+ .notNull()
535
+ .references(() => connections.id, { onDelete: "cascade" }),
536
+ operationId: uuid("operation_id").notNull(),
537
+ clientMessageId: uuid("client_message_id").notNull(),
538
+ targetKind: text("target_kind").$type<"channel" | "user">().notNull(),
539
+ targetId: text("target_id").notNull(),
540
+ requestDigest: text("request_digest").notNull(),
541
+ status: text("status").$type<"provider_started" | "completed">().notNull(),
542
+ claimHolderId: uuid("claim_holder_id"),
543
+ claimExpiresAt: timestamp("claim_expires_at", { withTimezone: true }),
544
+ attemptCount: integer("attempt_count").notNull().default(0),
545
+ lastFailureCode: text("last_failure_code"),
546
+ slackChannelId: text("slack_channel_id"),
547
+ slackMessageTimestamp: text("slack_message_timestamp"),
548
+ completedAt: timestamp("completed_at", { withTimezone: true }),
549
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
550
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
551
+ },
552
+ (table) => ({
553
+ workspaceOperation: uniqueIndex("slack_bot_post_operations_workspace_operation_uq").on(
554
+ table.workspaceId,
555
+ table.connectionId,
556
+ table.operationId,
557
+ ),
558
+ workspaceStatus: index("slack_bot_post_operations_workspace_status_idx").on(
559
+ table.workspaceId,
560
+ table.status,
561
+ table.updatedAt,
562
+ ),
563
+ targetKindValid: check(
564
+ "slack_bot_post_operations_target_kind_check",
565
+ sql`${table.targetKind} in ('channel', 'user')`,
566
+ ),
567
+ statusValid: check(
568
+ "slack_bot_post_operations_status_check",
569
+ sql`${table.status} in ('provider_started', 'completed')`,
570
+ ),
571
+ identityValid: check(
572
+ "slack_bot_post_operations_identity_check",
573
+ sql`${table.clientMessageId} = ${table.operationId}
574
+ and length(${table.targetId}) between 1 and 64
575
+ and ${table.requestDigest} ~ '^[0-9a-f]{64}$'
576
+ and ${table.attemptCount} > 0
577
+ and ((${table.claimHolderId} is null) = (${table.claimExpiresAt} is null))`,
578
+ ),
579
+ completionValid: check(
580
+ "slack_bot_post_operations_completion_check",
581
+ sql`(
582
+ ${table.status} = 'provider_started'
583
+ and ${table.slackChannelId} is null
584
+ and ${table.slackMessageTimestamp} is null
585
+ and ${table.completedAt} is null
586
+ ) or (
587
+ ${table.status} = 'completed'
588
+ and ${table.claimHolderId} is null
589
+ and ${table.claimExpiresAt} is null
590
+ and ${table.slackChannelId} is not null
591
+ and ${table.slackMessageTimestamp} is not null
592
+ and ${table.completedAt} is not null
593
+ )`,
594
+ ),
595
+ }),
596
+ );
597
+
513
598
  // OAuth client registrations minted through MCP DCR, keyed by authorization
514
599
  // server issuer. This is deployment-wide client identity, not a workspace
515
600
  // credential; per-user/provider tokens still live only in connections.
@@ -4333,3 +4418,5 @@ export const rigChanges = pgTable(
4333
4418
  workspaceStatus: index("rig_changes_workspace_status_idx").on(table.workspaceId, table.status),
4334
4419
  }),
4335
4420
  );
4421
+
4422
+ export * from "./workspace-instruction-policies-schema";