@opengeni/db 0.12.6 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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;
@@ -4723,6 +4756,8 @@ const connectionMetadataColumns = {
4723
4756
  lastUsedAt: schema.connections.lastUsedAt,
4724
4757
  lastError: schema.connections.lastError,
4725
4758
  version: schema.connections.version,
4759
+ verifiedInstallAt: schema.connections.verifiedInstallAt,
4760
+ verifiedInstallVersion: schema.connections.verifiedInstallVersion,
4726
4761
  metadata: schema.connections.metadata,
4727
4762
  createdBySubjectId: schema.connections.createdBySubjectId,
4728
4763
  updatedBySubjectId: schema.connections.updatedBySubjectId,
@@ -4739,7 +4774,7 @@ function connectionSubjectVisibility(subjectId?: string | null): SQL {
4739
4774
  export async function createConnection(
4740
4775
  db: Database,
4741
4776
  input: CreateConnectionInput,
4742
- ): Promise<ConnectionMetadata> {
4777
+ ): Promise<ConnectionMetadataWithVerification> {
4743
4778
  return await withRlsContext(
4744
4779
  db,
4745
4780
  { accountId: input.accountId, workspaceId: input.workspaceId },
@@ -4756,6 +4791,8 @@ export async function createConnection(
4756
4791
  credentialEncrypted: input.credentialEncrypted,
4757
4792
  grantedScopes: input.grantedScopes ?? [],
4758
4793
  expiresAt: input.expiresAt ?? null,
4794
+ verifiedInstallAt: input.verifiedInstallAt ?? null,
4795
+ verifiedInstallVersion: input.verifiedInstallVersion ?? null,
4759
4796
  metadata: input.metadata ?? {},
4760
4797
  createdBySubjectId: input.createdBySubjectId ?? null,
4761
4798
  updatedBySubjectId: input.updatedBySubjectId ?? input.createdBySubjectId ?? null,
@@ -4773,7 +4810,7 @@ export async function listConnectionsMetadata(
4773
4810
  db: Database,
4774
4811
  workspaceId: string,
4775
4812
  subjectId?: string | null,
4776
- ): Promise<ConnectionMetadata[]> {
4813
+ ): Promise<ConnectionMetadataWithVerification[]> {
4777
4814
  return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
4778
4815
  const rows = await scopedDb
4779
4816
  .select(connectionMetadataColumns)
@@ -4794,7 +4831,7 @@ export async function getConnectionMetadata(
4794
4831
  workspaceId: string,
4795
4832
  connectionId: string,
4796
4833
  subjectId?: string | null,
4797
- ): Promise<ConnectionMetadata | null> {
4834
+ ): Promise<ConnectionMetadataWithVerification | null> {
4798
4835
  return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
4799
4836
  const [row] = await scopedDb
4800
4837
  .select(connectionMetadataColumns)
@@ -4814,7 +4851,7 @@ export async function getConnectionMetadata(
4814
4851
  export async function updateConnection(
4815
4852
  db: Database,
4816
4853
  input: UpdateConnectionInput,
4817
- ): Promise<ConnectionMetadata | null> {
4854
+ ): Promise<ConnectionMetadataWithVerification | null> {
4818
4855
  return await withWorkspaceRls(db, input.workspaceId, async (scopedDb) => {
4819
4856
  const set = {
4820
4857
  updatedAt: new Date(),
@@ -4831,6 +4868,12 @@ export async function updateConnection(
4831
4868
  : {}),
4832
4869
  ...(input.grantedScopes !== undefined ? { grantedScopes: input.grantedScopes } : {}),
4833
4870
  ...(input.expiresAt !== undefined ? { expiresAt: input.expiresAt } : {}),
4871
+ ...(input.verifiedInstallAt !== undefined
4872
+ ? { verifiedInstallAt: input.verifiedInstallAt }
4873
+ : {}),
4874
+ ...(input.verifiedInstallVersion !== undefined
4875
+ ? { verifiedInstallVersion: input.verifiedInstallVersion }
4876
+ : {}),
4834
4877
  ...(input.metadata !== undefined ? { metadata: input.metadata } : {}),
4835
4878
  ...(input.updatedBySubjectId !== undefined
4836
4879
  ? { updatedBySubjectId: input.updatedBySubjectId }
@@ -4859,7 +4902,7 @@ export async function revokeConnection(
4859
4902
  workspaceId: string,
4860
4903
  connectionId: string,
4861
4904
  updatedBySubjectId?: string | null,
4862
- ): Promise<ConnectionMetadata | null> {
4905
+ ): Promise<ConnectionMetadataWithVerification | null> {
4863
4906
  return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
4864
4907
  const [row] = await scopedDb
4865
4908
  .update(schema.connections)
@@ -4868,6 +4911,13 @@ export async function revokeConnection(
4868
4911
  // The version bump invalidates any in-flight refresh's (id, version) CAS,
4869
4912
  // so a racing refresh cannot commit and flip the row back to active.
4870
4913
  version: sql`${schema.connections.version} + 1`,
4914
+ // Status-only revocation does not replace the verified credential or bot
4915
+ // identity. Carry the marker to the same new CAS version so the dedicated
4916
+ // reinstall path can still recognize (but not use) the inactive row.
4917
+ verifiedInstallVersion: sql`case
4918
+ when ${schema.connections.verifiedInstallAt} is null then null
4919
+ else ${schema.connections.version} + 1
4920
+ end`,
4871
4921
  updatedBySubjectId: updatedBySubjectId ?? null,
4872
4922
  updatedAt: new Date(),
4873
4923
  })
@@ -4886,6 +4936,272 @@ export async function revokeConnection(
4886
4936
  });
4887
4937
  }
4888
4938
 
4939
+ export type ClaimSlackBotPostOperationResult =
4940
+ | { kind: "claimed" | "in_progress" | "completed"; operation: SlackBotPostOperation }
4941
+ | { kind: "conflict" | "connection_not_found" };
4942
+
4943
+ /**
4944
+ * Claims one durable Slack post identity. The insert occurs before any provider
4945
+ * call; retries retain the original client_msg_id and immutable request digest.
4946
+ * A live claim suppresses concurrent sends, while a released/expired claim can
4947
+ * be reclaimed after response loss or process death.
4948
+ */
4949
+ export async function claimSlackBotPostOperation(
4950
+ db: Database,
4951
+ input: {
4952
+ accountId: string;
4953
+ workspaceId: string;
4954
+ connectionId: string;
4955
+ operationId: string;
4956
+ targetKind: "channel" | "user";
4957
+ targetId: string;
4958
+ requestDigest: string;
4959
+ claimHolderId: string;
4960
+ claimLeaseMs: number;
4961
+ },
4962
+ ): Promise<ClaimSlackBotPostOperationResult> {
4963
+ const claimLeaseMs = Math.max(1, Math.min(Math.trunc(input.claimLeaseMs), 120_000));
4964
+ return await withRlsContext(
4965
+ db,
4966
+ { accountId: input.accountId, workspaceId: input.workspaceId },
4967
+ async (scopedDb) =>
4968
+ await scopedDb.transaction(async (txRaw) => {
4969
+ const tx = txRaw as unknown as Database;
4970
+ const [connection] = await tx
4971
+ .select({ id: schema.connections.id })
4972
+ .from(schema.connections)
4973
+ .where(
4974
+ and(
4975
+ eq(schema.connections.accountId, input.accountId),
4976
+ eq(schema.connections.workspaceId, input.workspaceId),
4977
+ eq(schema.connections.id, input.connectionId),
4978
+ ),
4979
+ )
4980
+ .for("share")
4981
+ .limit(1);
4982
+ if (!connection) return { kind: "connection_not_found" } as const;
4983
+
4984
+ const [created] = await tx
4985
+ .insert(schema.slackBotPostOperations)
4986
+ .values({
4987
+ accountId: input.accountId,
4988
+ workspaceId: input.workspaceId,
4989
+ connectionId: input.connectionId,
4990
+ operationId: input.operationId,
4991
+ clientMessageId: input.operationId,
4992
+ targetKind: input.targetKind,
4993
+ targetId: input.targetId,
4994
+ requestDigest: input.requestDigest,
4995
+ status: "provider_started",
4996
+ claimHolderId: input.claimHolderId,
4997
+ claimExpiresAt: sql`now() + (${claimLeaseMs} * interval '1 millisecond')`,
4998
+ attemptCount: 1,
4999
+ })
5000
+ .onConflictDoNothing({
5001
+ target: [
5002
+ schema.slackBotPostOperations.workspaceId,
5003
+ schema.slackBotPostOperations.connectionId,
5004
+ schema.slackBotPostOperations.operationId,
5005
+ ],
5006
+ })
5007
+ .returning();
5008
+ if (created) {
5009
+ return { kind: "claimed", operation: mapSlackBotPostOperation(created) } as const;
5010
+ }
5011
+
5012
+ const [existing] = await tx
5013
+ .select()
5014
+ .from(schema.slackBotPostOperations)
5015
+ .where(
5016
+ and(
5017
+ eq(schema.slackBotPostOperations.workspaceId, input.workspaceId),
5018
+ eq(schema.slackBotPostOperations.connectionId, input.connectionId),
5019
+ eq(schema.slackBotPostOperations.operationId, input.operationId),
5020
+ ),
5021
+ )
5022
+ .for("update")
5023
+ .limit(1);
5024
+ if (!existing) throw new Error("Slack post operation disappeared after conflict");
5025
+ if (
5026
+ existing.accountId !== input.accountId ||
5027
+ existing.targetKind !== input.targetKind ||
5028
+ existing.targetId !== input.targetId ||
5029
+ existing.requestDigest !== input.requestDigest ||
5030
+ existing.clientMessageId !== input.operationId
5031
+ ) {
5032
+ return { kind: "conflict" } as const;
5033
+ }
5034
+ if (existing.status === "completed") {
5035
+ return { kind: "completed", operation: mapSlackBotPostOperation(existing) } as const;
5036
+ }
5037
+
5038
+ const [reclaimed] = await tx
5039
+ .update(schema.slackBotPostOperations)
5040
+ .set({
5041
+ claimHolderId: input.claimHolderId,
5042
+ claimExpiresAt: sql`now() + (${claimLeaseMs} * interval '1 millisecond')`,
5043
+ attemptCount: sql`${schema.slackBotPostOperations.attemptCount} + 1`,
5044
+ lastFailureCode: null,
5045
+ updatedAt: sql`now()`,
5046
+ })
5047
+ .where(
5048
+ and(
5049
+ eq(schema.slackBotPostOperations.id, existing.id),
5050
+ or(
5051
+ isNull(schema.slackBotPostOperations.claimHolderId),
5052
+ lte(schema.slackBotPostOperations.claimExpiresAt, sql`now()`),
5053
+ ),
5054
+ ),
5055
+ )
5056
+ .returning();
5057
+ return reclaimed
5058
+ ? ({ kind: "claimed", operation: mapSlackBotPostOperation(reclaimed) } as const)
5059
+ : ({ kind: "in_progress", operation: mapSlackBotPostOperation(existing) } as const);
5060
+ }),
5061
+ );
5062
+ }
5063
+
5064
+ export async function releaseSlackBotPostOperationClaim(
5065
+ db: Database,
5066
+ input: {
5067
+ accountId: string;
5068
+ workspaceId: string;
5069
+ connectionId: string;
5070
+ operationId: string;
5071
+ claimHolderId: string;
5072
+ failureCode: string;
5073
+ },
5074
+ ): Promise<boolean> {
5075
+ return await withRlsContext(
5076
+ db,
5077
+ { accountId: input.accountId, workspaceId: input.workspaceId },
5078
+ async (scopedDb) => {
5079
+ const rows = await scopedDb
5080
+ .update(schema.slackBotPostOperations)
5081
+ .set({
5082
+ claimHolderId: null,
5083
+ claimExpiresAt: null,
5084
+ lastFailureCode: input.failureCode.slice(0, 128),
5085
+ updatedAt: sql`now()`,
5086
+ })
5087
+ .where(
5088
+ and(
5089
+ eq(schema.slackBotPostOperations.workspaceId, input.workspaceId),
5090
+ eq(schema.slackBotPostOperations.connectionId, input.connectionId),
5091
+ eq(schema.slackBotPostOperations.operationId, input.operationId),
5092
+ eq(schema.slackBotPostOperations.status, "provider_started"),
5093
+ eq(schema.slackBotPostOperations.claimHolderId, input.claimHolderId),
5094
+ ),
5095
+ )
5096
+ .returning({ id: schema.slackBotPostOperations.id });
5097
+ return rows.length === 1;
5098
+ },
5099
+ );
5100
+ }
5101
+
5102
+ export type CompleteSlackBotPostOperationResult =
5103
+ | { kind: "completed"; operation: SlackBotPostOperation; newlyCompleted: boolean }
5104
+ | { kind: "not_found" | "not_owned" };
5105
+
5106
+ /** Completion and the single success audit receipt commit atomically. */
5107
+ export async function completeSlackBotPostOperation(
5108
+ db: Database,
5109
+ input: {
5110
+ accountId: string;
5111
+ workspaceId: string;
5112
+ connectionId: string;
5113
+ operationId: string;
5114
+ claimHolderId: string;
5115
+ slackChannelId: string;
5116
+ slackMessageTimestamp: string;
5117
+ subjectId?: string | null;
5118
+ auditMetadata: Record<string, unknown>;
5119
+ },
5120
+ ): Promise<CompleteSlackBotPostOperationResult> {
5121
+ return await withRlsContext(
5122
+ db,
5123
+ { accountId: input.accountId, workspaceId: input.workspaceId },
5124
+ async (scopedDb) =>
5125
+ await scopedDb.transaction(async (txRaw) => {
5126
+ const tx = txRaw as unknown as Database;
5127
+ const [current] = await tx
5128
+ .select()
5129
+ .from(schema.slackBotPostOperations)
5130
+ .where(
5131
+ and(
5132
+ eq(schema.slackBotPostOperations.workspaceId, input.workspaceId),
5133
+ eq(schema.slackBotPostOperations.connectionId, input.connectionId),
5134
+ eq(schema.slackBotPostOperations.operationId, input.operationId),
5135
+ ),
5136
+ )
5137
+ .for("update")
5138
+ .limit(1);
5139
+ if (!current) return { kind: "not_found" } as const;
5140
+ if (current.status === "completed") {
5141
+ return {
5142
+ kind: "completed",
5143
+ operation: mapSlackBotPostOperation(current),
5144
+ newlyCompleted: false,
5145
+ } as const;
5146
+ }
5147
+ if (current.claimHolderId !== input.claimHolderId) {
5148
+ return { kind: "not_owned" } as const;
5149
+ }
5150
+ const [completed] = await tx
5151
+ .update(schema.slackBotPostOperations)
5152
+ .set({
5153
+ status: "completed",
5154
+ claimHolderId: null,
5155
+ claimExpiresAt: null,
5156
+ lastFailureCode: null,
5157
+ slackChannelId: input.slackChannelId,
5158
+ slackMessageTimestamp: input.slackMessageTimestamp,
5159
+ completedAt: sql`now()`,
5160
+ updatedAt: sql`now()`,
5161
+ })
5162
+ .where(eq(schema.slackBotPostOperations.id, current.id))
5163
+ .returning();
5164
+ if (!completed) throw new Error("Slack post completion returned no row");
5165
+ await tx.insert(schema.auditEvents).values({
5166
+ accountId: input.accountId,
5167
+ workspaceId: input.workspaceId,
5168
+ subjectId: input.subjectId ?? null,
5169
+ action: "slack_bot.message.post",
5170
+ targetType: "connection",
5171
+ targetId: input.connectionId,
5172
+ metadata: input.auditMetadata,
5173
+ });
5174
+ return {
5175
+ kind: "completed",
5176
+ operation: mapSlackBotPostOperation(completed),
5177
+ newlyCompleted: true,
5178
+ } as const;
5179
+ }),
5180
+ );
5181
+ }
5182
+
5183
+ export async function getSlackBotPostOperation(
5184
+ db: Database,
5185
+ workspaceId: string,
5186
+ connectionId: string,
5187
+ operationId: string,
5188
+ ): Promise<SlackBotPostOperation | null> {
5189
+ return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
5190
+ const [row] = await scopedDb
5191
+ .select()
5192
+ .from(schema.slackBotPostOperations)
5193
+ .where(
5194
+ and(
5195
+ eq(schema.slackBotPostOperations.workspaceId, workspaceId),
5196
+ eq(schema.slackBotPostOperations.connectionId, connectionId),
5197
+ eq(schema.slackBotPostOperations.operationId, operationId),
5198
+ ),
5199
+ )
5200
+ .limit(1);
5201
+ return row ? mapSlackBotPostOperation(row) : null;
5202
+ });
5203
+ }
5204
+
4889
5205
  export async function loadConnectionCredentialForBroker(
4890
5206
  db: Database,
4891
5207
  settings: Settings,
@@ -5019,6 +5335,10 @@ export async function setConnectionStatus(
5019
5335
  status,
5020
5336
  lastError,
5021
5337
  version: sql`${schema.connections.version} + 1`,
5338
+ verifiedInstallVersion: sql`case
5339
+ when ${schema.connections.verifiedInstallAt} is null then null
5340
+ else ${schema.connections.version} + 1
5341
+ end`,
5022
5342
  updatedAt: new Date(),
5023
5343
  })
5024
5344
  .where(
@@ -35402,12 +35722,14 @@ function mapConnectionMetadata(row: {
35402
35722
  lastUsedAt: Date | null;
35403
35723
  lastError: string | null;
35404
35724
  version: number;
35725
+ verifiedInstallAt: Date | null;
35726
+ verifiedInstallVersion: number | null;
35405
35727
  metadata: Record<string, unknown>;
35406
35728
  createdBySubjectId: string | null;
35407
35729
  updatedBySubjectId: string | null;
35408
35730
  createdAt: Date;
35409
35731
  updatedAt: Date;
35410
- }): ConnectionMetadata {
35732
+ }): ConnectionMetadataWithVerification {
35411
35733
  return {
35412
35734
  id: row.id,
35413
35735
  accountId: row.accountId,
@@ -35422,6 +35744,8 @@ function mapConnectionMetadata(row: {
35422
35744
  lastUsedAt: row.lastUsedAt?.toISOString() ?? null,
35423
35745
  lastError: row.lastError,
35424
35746
  version: row.version,
35747
+ verifiedInstallAt: row.verifiedInstallAt?.toISOString() ?? null,
35748
+ verifiedInstallVersion: row.verifiedInstallVersion,
35425
35749
  metadata: row.metadata,
35426
35750
  createdBySubjectId: row.createdBySubjectId,
35427
35751
  updatedBySubjectId: row.updatedBySubjectId,
@@ -35430,6 +35754,32 @@ function mapConnectionMetadata(row: {
35430
35754
  };
35431
35755
  }
35432
35756
 
35757
+ function mapSlackBotPostOperation(
35758
+ row: typeof schema.slackBotPostOperations.$inferSelect,
35759
+ ): SlackBotPostOperation {
35760
+ return {
35761
+ id: row.id,
35762
+ accountId: row.accountId,
35763
+ workspaceId: row.workspaceId,
35764
+ connectionId: row.connectionId,
35765
+ operationId: row.operationId,
35766
+ clientMessageId: row.clientMessageId,
35767
+ targetKind: row.targetKind,
35768
+ targetId: row.targetId,
35769
+ requestDigest: row.requestDigest,
35770
+ status: row.status,
35771
+ claimHolderId: row.claimHolderId,
35772
+ claimExpiresAt: row.claimExpiresAt,
35773
+ attemptCount: row.attemptCount,
35774
+ lastFailureCode: row.lastFailureCode,
35775
+ slackChannelId: row.slackChannelId,
35776
+ slackMessageTimestamp: row.slackMessageTimestamp,
35777
+ completedAt: row.completedAt,
35778
+ createdAt: row.createdAt,
35779
+ updatedAt: row.updatedAt,
35780
+ };
35781
+ }
35782
+
35433
35783
  function mapKnowledgeMemory(row: typeof schema.knowledgeMemories.$inferSelect): KnowledgeMemory {
35434
35784
  return {
35435
35785
  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";