@opengeni/db 0.21.0 → 0.22.2

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
@@ -146,7 +146,12 @@ import {
146
146
  SubmitHumanInputResponseRequest,
147
147
  TurnExecutionPolicyV1,
148
148
  } from "@opengeni/contracts";
149
- import { environmentsEncryptionKeyBytes, type Settings } from "@opengeni/config";
149
+ import {
150
+ environmentsEncryptionKeyBytes,
151
+ VERCEL_AI_GATEWAY_CONNECTION_DOMAIN,
152
+ VERCEL_AI_GATEWAY_CONNECTION_ROLE,
153
+ type Settings,
154
+ } from "@opengeni/config";
150
155
  import { boundModelToolOutputItem, isCodexBilledModel } from "@opengeni/codex";
151
156
  // Re-exported so consumers get the whole codex-billed detection surface (the pure
152
157
  // prefix test + the credential-aware predicates below) from a single import.
@@ -7237,6 +7242,56 @@ export async function loadConnectionCredentialForBroker(
7237
7242
  );
7238
7243
  }
7239
7244
 
7245
+ export async function workspaceVercelAiGatewayConnectionActive(
7246
+ db: Database,
7247
+ workspaceId: string,
7248
+ ): Promise<boolean> {
7249
+ const connections = await listConnectionsMetadata(db, workspaceId, null);
7250
+ return connections.some(
7251
+ (connection) =>
7252
+ connection.subjectId === null &&
7253
+ connection.providerDomain === VERCEL_AI_GATEWAY_CONNECTION_DOMAIN &&
7254
+ connection.kind === "api_key" &&
7255
+ connection.status === "active" &&
7256
+ connection.metadata.credentialRole === VERCEL_AI_GATEWAY_CONNECTION_ROLE,
7257
+ );
7258
+ }
7259
+
7260
+ /** Resolve only the reviewed workspace-shared AI Gateway credential shape. */
7261
+ export async function loadWorkspaceVercelAiGatewayApiKey(
7262
+ db: Database,
7263
+ settings: Settings,
7264
+ workspaceId: string,
7265
+ ): Promise<string | null> {
7266
+ const metadata = (await listConnectionsMetadata(db, workspaceId, null)).find(
7267
+ (connection) =>
7268
+ connection.subjectId === null &&
7269
+ connection.providerDomain === VERCEL_AI_GATEWAY_CONNECTION_DOMAIN &&
7270
+ connection.kind === "api_key" &&
7271
+ connection.status === "active" &&
7272
+ connection.metadata.credentialRole === VERCEL_AI_GATEWAY_CONNECTION_ROLE,
7273
+ );
7274
+ if (!metadata) {
7275
+ return null;
7276
+ }
7277
+ const connection = await loadConnectionCredentialForBroker(db, settings, {
7278
+ workspaceId,
7279
+ connectionId: metadata.id,
7280
+ providerDomain: VERCEL_AI_GATEWAY_CONNECTION_DOMAIN,
7281
+ kind: "api_key",
7282
+ allowSubjectOwned: false,
7283
+ });
7284
+ if (
7285
+ !connection ||
7286
+ connection.status !== "active" ||
7287
+ connection.metadata.credentialRole !== VERCEL_AI_GATEWAY_CONNECTION_ROLE
7288
+ ) {
7289
+ return null;
7290
+ }
7291
+ const apiKey = connection.credential.apiKey;
7292
+ return typeof apiKey === "string" && apiKey.trim().length > 0 ? apiKey : null;
7293
+ }
7294
+
7240
7295
  export async function recordConnectionTokenRefresh(
7241
7296
  db: Database,
7242
7297
  input: {
@@ -15746,6 +15801,816 @@ export async function listSessionMcpServersForRun(
15746
15801
  });
15747
15802
  }
15748
15803
 
15804
+ export type ConnectorActionPolicyDecision = schema.ConnectorActionPolicyDecision;
15805
+ export type ConnectorActionPolicySnapshotEntry = schema.ConnectorActionPolicySnapshotEntry;
15806
+
15807
+ export type ConnectorActionAttemptIdentity = {
15808
+ accountId: string;
15809
+ workspaceId: string;
15810
+ sessionId: string;
15811
+ turnId: string;
15812
+ attemptId: string;
15813
+ executionGeneration: number;
15814
+ initiator: Pick<TurnInitiator, "kind" | "subjectId">;
15815
+ };
15816
+
15817
+ export type ConnectorActionInvocation = {
15818
+ approvalId: string;
15819
+ connectionId?: string | null;
15820
+ serverId: string;
15821
+ toolName: string;
15822
+ arguments: unknown;
15823
+ };
15824
+
15825
+ export type PrepareConnectorActionApprovalResult =
15826
+ | { managed: false; decision: "unmanaged" }
15827
+ | {
15828
+ managed: true;
15829
+ decision: ConnectorActionPolicyDecision;
15830
+ requestId?: string;
15831
+ actionFingerprint: string;
15832
+ };
15833
+
15834
+ export type BeginConnectorActionExecutionResult =
15835
+ | { allowed: true; managed: false }
15836
+ | {
15837
+ allowed: true;
15838
+ managed: true;
15839
+ requestId: string;
15840
+ actionFingerprint: string;
15841
+ }
15842
+ | {
15843
+ allowed: false;
15844
+ managed: true;
15845
+ reason: "approval_required" | "blocked" | "rejected" | "already_executed" | "uncertain_retry";
15846
+ requestId: string;
15847
+ actionFingerprint: string;
15848
+ };
15849
+
15850
+ const CONNECTOR_ACTION_POLICY_SNAPSHOT_MAX = 2048;
15851
+ const CONNECTOR_ACTION_APPROVAL_ID_MAX = 1024;
15852
+ const CONNECTOR_ACTION_CONNECTION_ID_MAX = 512;
15853
+ const CONNECTOR_ACTION_SERVER_ID_MAX = 256;
15854
+ const CONNECTOR_ACTION_NAME_MAX = 512;
15855
+
15856
+ function boundedConnectorActionText(value: string, label: string, max: number): string {
15857
+ const trimmed = value.trim();
15858
+ if (trimmed.length === 0 || Buffer.byteLength(trimmed, "utf8") > max) {
15859
+ throw new Error(`${label} must be between 1 and ${max} UTF-8 bytes`);
15860
+ }
15861
+ return trimmed;
15862
+ }
15863
+
15864
+ /**
15865
+ * Resolve the request's policy selector. This caller-controlled value is used
15866
+ * only transiently to match the attempt-frozen policy snapshot; it must never
15867
+ * be copied into a request row or audit event.
15868
+ */
15869
+ function connectorActionPolicySelector(toolName: string, args: unknown): string {
15870
+ if (args && typeof args === "object" && !Array.isArray(args)) {
15871
+ const action = (args as Record<string, unknown>).action;
15872
+ if (typeof action === "string" && action.trim().length > 0) {
15873
+ return boundedConnectorActionText(action, "connector action name", CONNECTOR_ACTION_NAME_MAX);
15874
+ }
15875
+ }
15876
+ return toolName;
15877
+ }
15878
+
15879
+ function connectorActionEvidenceName(
15880
+ resolved: Exclude<ResolvedConnectorActionPolicy, { managed: false }>,
15881
+ ): string {
15882
+ return resolved.entry?.actionName ?? "*";
15883
+ }
15884
+
15885
+ function connectorActionFingerprint(input: {
15886
+ workspaceId: string;
15887
+ connectionId: string;
15888
+ serverId: string;
15889
+ toolName: string;
15890
+ actionName: string;
15891
+ arguments: unknown;
15892
+ }): string {
15893
+ return createHash("sha256")
15894
+ .update(
15895
+ stableJson({
15896
+ workspaceId: input.workspaceId,
15897
+ connectionId: input.connectionId,
15898
+ serverId: input.serverId,
15899
+ toolName: input.toolName,
15900
+ actionName: input.actionName,
15901
+ arguments: input.arguments ?? null,
15902
+ }),
15903
+ "utf8",
15904
+ )
15905
+ .digest("hex");
15906
+ }
15907
+
15908
+ type ResolvedConnectorActionPolicy =
15909
+ | { managed: false }
15910
+ | {
15911
+ managed: true;
15912
+ source: "explicit";
15913
+ entry: ConnectorActionPolicySnapshotEntry;
15914
+ }
15915
+ | {
15916
+ managed: true;
15917
+ source: "ambiguous";
15918
+ entry: null;
15919
+ decision: "block";
15920
+ };
15921
+
15922
+ /** Resolve one immutable attempt snapshot with exact-over-wildcard precedence. */
15923
+ export function resolveConnectorActionPolicy(
15924
+ snapshot: readonly ConnectorActionPolicySnapshotEntry[],
15925
+ input: { connectionId: string; serverId: string; toolName: string; actionName: string },
15926
+ ): ResolvedConnectorActionPolicy {
15927
+ const candidates = snapshot
15928
+ .filter(
15929
+ (entry) =>
15930
+ entry.connectionId === input.connectionId &&
15931
+ (entry.serverId === input.serverId || entry.serverId === "*") &&
15932
+ (entry.toolName === input.toolName || entry.toolName === "*") &&
15933
+ (entry.actionName === input.actionName || entry.actionName === "*"),
15934
+ )
15935
+ .map((entry) => ({
15936
+ entry,
15937
+ specificity:
15938
+ Number(entry.serverId !== "*") +
15939
+ Number(entry.toolName !== "*") +
15940
+ Number(entry.actionName !== "*"),
15941
+ }))
15942
+ .sort(
15943
+ (left, right) =>
15944
+ right.specificity - left.specificity || left.entry.id.localeCompare(right.entry.id),
15945
+ );
15946
+ const selected = candidates[0];
15947
+ if (!selected) return { managed: false };
15948
+ if (candidates[1]?.specificity === selected.specificity) {
15949
+ return { managed: true, source: "ambiguous", entry: null, decision: "block" };
15950
+ }
15951
+ return { managed: true, source: "explicit", entry: selected.entry };
15952
+ }
15953
+
15954
+ function connectorActionAuditMetadata(
15955
+ row: typeof schema.connectorActionRequests.$inferSelect,
15956
+ extra: Record<string, unknown> = {},
15957
+ ): Record<string, unknown> {
15958
+ return {
15959
+ requestId: row.id,
15960
+ sessionId: row.sessionId,
15961
+ turnId: row.turnId,
15962
+ attemptId: row.creationAttemptId,
15963
+ creationExecutionGeneration: row.creationExecutionGeneration,
15964
+ executionAttemptId: row.executionAttemptId,
15965
+ executionAttemptGeneration: row.executionAttemptGeneration,
15966
+ approvalId: row.approvalId,
15967
+ initiatorKind: row.initiatorKind,
15968
+ initiatorSubjectId: row.initiatorSubjectId,
15969
+ connectionId: row.connectionId,
15970
+ connectionVersion: row.connectionVersion,
15971
+ serverId: row.serverId,
15972
+ toolName: row.toolName,
15973
+ actionName: row.actionName,
15974
+ policyId: row.policyId,
15975
+ policyVersion: row.policyVersion,
15976
+ policySource: row.policySource,
15977
+ policyDecision: row.policyDecision,
15978
+ actionFingerprint: row.actionFingerprint,
15979
+ ...extra,
15980
+ };
15981
+ }
15982
+
15983
+ async function insertConnectorActionAudit(
15984
+ db: Database,
15985
+ input: {
15986
+ row: typeof schema.connectorActionRequests.$inferSelect;
15987
+ action: string;
15988
+ subjectId: string;
15989
+ extra?: Record<string, unknown>;
15990
+ },
15991
+ ): Promise<void> {
15992
+ await db.insert(schema.auditEvents).values({
15993
+ accountId: input.row.accountId,
15994
+ workspaceId: input.row.workspaceId,
15995
+ subjectId: input.subjectId,
15996
+ action: input.action,
15997
+ targetType: "connector_action_request",
15998
+ targetId: input.row.id,
15999
+ metadata: connectorActionAuditMetadata(input.row, input.extra),
16000
+ });
16001
+ }
16002
+
16003
+ function normalizedConnectorActionInvocation(
16004
+ identity: ConnectorActionAttemptIdentity,
16005
+ invocation: ConnectorActionInvocation,
16006
+ ): {
16007
+ approvalId: string;
16008
+ connectionId: string | null;
16009
+ serverId: string;
16010
+ toolName: string;
16011
+ policyActionSelector: string;
16012
+ arguments: unknown;
16013
+ } {
16014
+ const approvalId = boundedConnectorActionText(
16015
+ invocation.approvalId,
16016
+ "connector approval id",
16017
+ CONNECTOR_ACTION_APPROVAL_ID_MAX,
16018
+ );
16019
+ const serverId = boundedConnectorActionText(
16020
+ invocation.serverId,
16021
+ "connector server id",
16022
+ CONNECTOR_ACTION_SERVER_ID_MAX,
16023
+ );
16024
+ const toolName = boundedConnectorActionText(
16025
+ invocation.toolName,
16026
+ "connector tool name",
16027
+ CONNECTOR_ACTION_NAME_MAX,
16028
+ );
16029
+ const policyActionSelector = connectorActionPolicySelector(toolName, invocation.arguments);
16030
+ const connectionId = invocation.connectionId?.trim()
16031
+ ? boundedConnectorActionText(
16032
+ invocation.connectionId,
16033
+ "connector connection id",
16034
+ CONNECTOR_ACTION_CONNECTION_ID_MAX,
16035
+ )
16036
+ : null;
16037
+ return {
16038
+ approvalId,
16039
+ connectionId,
16040
+ serverId,
16041
+ toolName,
16042
+ policyActionSelector,
16043
+ arguments: invocation.arguments,
16044
+ };
16045
+ }
16046
+
16047
+ function durableConnectorActionInvocation(
16048
+ identity: ConnectorActionAttemptIdentity,
16049
+ invocation: ReturnType<typeof normalizedConnectorActionInvocation> & { connectionId: string },
16050
+ resolved: Exclude<ResolvedConnectorActionPolicy, { managed: false }>,
16051
+ ): {
16052
+ approvalId: string;
16053
+ connectionId: string;
16054
+ serverId: string;
16055
+ toolName: string;
16056
+ actionName: string;
16057
+ actionFingerprint: string;
16058
+ } {
16059
+ const actionName = connectorActionEvidenceName(resolved);
16060
+ return {
16061
+ approvalId: invocation.approvalId,
16062
+ connectionId: invocation.connectionId,
16063
+ serverId: invocation.serverId,
16064
+ toolName: invocation.toolName,
16065
+ actionName,
16066
+ actionFingerprint: connectorActionFingerprint({
16067
+ workspaceId: identity.workspaceId,
16068
+ connectionId: invocation.connectionId,
16069
+ serverId: invocation.serverId,
16070
+ toolName: invocation.toolName,
16071
+ actionName,
16072
+ arguments: invocation.arguments,
16073
+ }),
16074
+ };
16075
+ }
16076
+
16077
+ async function connectorActionAttemptSnapshot(
16078
+ db: Database,
16079
+ identity: ConnectorActionAttemptIdentity,
16080
+ ): Promise<ConnectorActionPolicySnapshotEntry[]> {
16081
+ const [attempt] = await db
16082
+ .select({
16083
+ accountId: schema.sessionTurnAttempts.accountId,
16084
+ sessionId: schema.sessionTurnAttempts.sessionId,
16085
+ turnId: schema.sessionTurnAttempts.turnId,
16086
+ executionGeneration: schema.sessionTurnAttempts.executionGeneration,
16087
+ state: schema.sessionTurnAttempts.state,
16088
+ connectorActionPolicies: schema.sessionTurnAttempts.connectorActionPolicies,
16089
+ })
16090
+ .from(schema.sessionTurnAttempts)
16091
+ .where(
16092
+ and(
16093
+ eq(schema.sessionTurnAttempts.workspaceId, identity.workspaceId),
16094
+ eq(schema.sessionTurnAttempts.id, identity.attemptId),
16095
+ ),
16096
+ )
16097
+ .limit(1);
16098
+ if (
16099
+ !attempt ||
16100
+ attempt.accountId !== identity.accountId ||
16101
+ attempt.sessionId !== identity.sessionId ||
16102
+ attempt.turnId !== identity.turnId ||
16103
+ attempt.executionGeneration !== identity.executionGeneration ||
16104
+ !["claimed", "running"].includes(attempt.state)
16105
+ ) {
16106
+ throw new Error(`connector action attempt ownership is unavailable: ${identity.attemptId}`);
16107
+ }
16108
+ if (attempt.connectorActionPolicies.length > CONNECTOR_ACTION_POLICY_SNAPSHOT_MAX) {
16109
+ throw new Error("connector action policy snapshot exceeds the runtime bound");
16110
+ }
16111
+ return attempt.connectorActionPolicies;
16112
+ }
16113
+
16114
+ function connectorActionRequestMatches(
16115
+ row: typeof schema.connectorActionRequests.$inferSelect,
16116
+ input: {
16117
+ identity: ConnectorActionAttemptIdentity;
16118
+ invocation: ReturnType<typeof durableConnectorActionInvocation>;
16119
+ resolved: Exclude<ResolvedConnectorActionPolicy, { managed: false }>;
16120
+ },
16121
+ ): boolean {
16122
+ const entry = input.resolved.entry;
16123
+ return (
16124
+ row.accountId === input.identity.accountId &&
16125
+ row.workspaceId === input.identity.workspaceId &&
16126
+ row.sessionId === input.identity.sessionId &&
16127
+ row.turnId === input.identity.turnId &&
16128
+ row.creationAttemptId === input.identity.attemptId &&
16129
+ row.creationExecutionGeneration === input.identity.executionGeneration &&
16130
+ row.approvalId === input.invocation.approvalId &&
16131
+ row.initiatorKind === input.identity.initiator.kind &&
16132
+ row.initiatorSubjectId === input.identity.initiator.subjectId &&
16133
+ row.connectionId === input.invocation.connectionId &&
16134
+ row.serverId === input.invocation.serverId &&
16135
+ row.toolName === input.invocation.toolName &&
16136
+ row.actionName === input.invocation.actionName &&
16137
+ row.policyId === (entry?.id ?? null) &&
16138
+ row.policyVersion === (entry?.version ?? null) &&
16139
+ row.policySource === input.resolved.source &&
16140
+ row.policyDecision === (entry?.policy ?? "block") &&
16141
+ row.actionFingerprint === input.invocation.actionFingerprint
16142
+ );
16143
+ }
16144
+
16145
+ function connectorActionRequestMatchesLogicalCall(
16146
+ row: typeof schema.connectorActionRequests.$inferSelect,
16147
+ identity: ConnectorActionAttemptIdentity,
16148
+ invocation: ReturnType<typeof normalizedConnectorActionInvocation>,
16149
+ ): boolean {
16150
+ if (!invocation.connectionId) return false;
16151
+ return (
16152
+ row.accountId === identity.accountId &&
16153
+ row.workspaceId === identity.workspaceId &&
16154
+ row.sessionId === identity.sessionId &&
16155
+ row.turnId === identity.turnId &&
16156
+ row.approvalId === invocation.approvalId &&
16157
+ row.initiatorKind === identity.initiator.kind &&
16158
+ row.initiatorSubjectId === identity.initiator.subjectId &&
16159
+ row.connectionId === invocation.connectionId &&
16160
+ row.serverId === invocation.serverId &&
16161
+ row.toolName === invocation.toolName &&
16162
+ row.actionFingerprint ===
16163
+ connectorActionFingerprint({
16164
+ workspaceId: identity.workspaceId,
16165
+ connectionId: invocation.connectionId,
16166
+ serverId: invocation.serverId,
16167
+ toolName: invocation.toolName,
16168
+ actionName: row.actionName,
16169
+ arguments: invocation.arguments,
16170
+ })
16171
+ );
16172
+ }
16173
+
16174
+ async function insertConnectorActionRequest(
16175
+ db: Database,
16176
+ input: {
16177
+ identity: ConnectorActionAttemptIdentity;
16178
+ invocation: ReturnType<typeof durableConnectorActionInvocation>;
16179
+ resolved: Exclude<ResolvedConnectorActionPolicy, { managed: false }>;
16180
+ status: "pending" | "blocked" | "executing";
16181
+ },
16182
+ ): Promise<{ row: typeof schema.connectorActionRequests.$inferSelect; inserted: boolean }> {
16183
+ const entry = input.resolved.entry;
16184
+ const [inserted] = await db
16185
+ .insert(schema.connectorActionRequests)
16186
+ .values({
16187
+ accountId: input.identity.accountId,
16188
+ workspaceId: input.identity.workspaceId,
16189
+ sessionId: input.identity.sessionId,
16190
+ turnId: input.identity.turnId,
16191
+ creationAttemptId: input.identity.attemptId,
16192
+ creationExecutionGeneration: input.identity.executionGeneration,
16193
+ approvalId: input.invocation.approvalId,
16194
+ initiatorKind: input.identity.initiator.kind,
16195
+ initiatorSubjectId: input.identity.initiator.subjectId,
16196
+ connectionId: input.invocation.connectionId!,
16197
+ serverId: input.invocation.serverId,
16198
+ toolName: input.invocation.toolName,
16199
+ actionName: input.invocation.actionName,
16200
+ policyId: entry?.id ?? null,
16201
+ policyVersion: entry?.version ?? null,
16202
+ policySource: input.resolved.source,
16203
+ policyDecision: entry?.policy ?? "block",
16204
+ actionFingerprint: input.invocation.actionFingerprint!,
16205
+ status: input.status,
16206
+ ...(input.status === "executing"
16207
+ ? {
16208
+ executionAttemptId: input.identity.attemptId,
16209
+ executionAttemptGeneration: input.identity.executionGeneration,
16210
+ executionStartedAt: new Date(),
16211
+ }
16212
+ : {}),
16213
+ })
16214
+ .onConflictDoNothing({
16215
+ target: [
16216
+ schema.connectorActionRequests.workspaceId,
16217
+ schema.connectorActionRequests.sessionId,
16218
+ schema.connectorActionRequests.turnId,
16219
+ schema.connectorActionRequests.approvalId,
16220
+ ],
16221
+ })
16222
+ .returning();
16223
+ if (inserted) return { row: inserted, inserted: true };
16224
+ const [existing] = await db
16225
+ .select()
16226
+ .from(schema.connectorActionRequests)
16227
+ .where(
16228
+ and(
16229
+ eq(schema.connectorActionRequests.workspaceId, input.identity.workspaceId),
16230
+ eq(schema.connectorActionRequests.sessionId, input.identity.sessionId),
16231
+ eq(schema.connectorActionRequests.turnId, input.identity.turnId),
16232
+ eq(schema.connectorActionRequests.approvalId, input.invocation.approvalId),
16233
+ ),
16234
+ )
16235
+ .for("update")
16236
+ .limit(1);
16237
+ if (!existing || !connectorActionRequestMatches(existing, input)) {
16238
+ throw new Error("connector action approval id conflicts with different immutable inputs");
16239
+ }
16240
+ return { row: existing, inserted: false };
16241
+ }
16242
+
16243
+ export async function upsertConnectorActionPolicy(
16244
+ db: Database,
16245
+ input: {
16246
+ accountId: string;
16247
+ workspaceId: string;
16248
+ subjectId: string;
16249
+ connectionId: string;
16250
+ serverId: string;
16251
+ toolName: string;
16252
+ actionName: string;
16253
+ policy: ConnectorActionPolicyDecision;
16254
+ },
16255
+ ): Promise<{ policy: typeof schema.connectorActionPolicies.$inferSelect; changed: boolean }> {
16256
+ const scope = {
16257
+ connectionId: boundedConnectorActionText(
16258
+ input.connectionId,
16259
+ "connector connection id",
16260
+ CONNECTOR_ACTION_CONNECTION_ID_MAX,
16261
+ ),
16262
+ serverId: boundedConnectorActionText(
16263
+ input.serverId,
16264
+ "connector server id",
16265
+ CONNECTOR_ACTION_SERVER_ID_MAX,
16266
+ ),
16267
+ toolName: boundedConnectorActionText(
16268
+ input.toolName,
16269
+ "connector tool name",
16270
+ CONNECTOR_ACTION_NAME_MAX,
16271
+ ),
16272
+ actionName: boundedConnectorActionText(
16273
+ input.actionName,
16274
+ "connector action name",
16275
+ CONNECTOR_ACTION_NAME_MAX,
16276
+ ),
16277
+ };
16278
+ const subjectId = boundedConnectorActionText(input.subjectId, "policy actor", 1024);
16279
+ return await withRlsContext(
16280
+ db,
16281
+ { accountId: input.accountId, workspaceId: input.workspaceId },
16282
+ async (scopedDb) =>
16283
+ await scopedDb.transaction(async (tx) => {
16284
+ await assertWorkspaceAccountPairInScope(tx, input.accountId, input.workspaceId);
16285
+ const [existing] = await tx
16286
+ .select()
16287
+ .from(schema.connectorActionPolicies)
16288
+ .where(
16289
+ and(
16290
+ eq(schema.connectorActionPolicies.workspaceId, input.workspaceId),
16291
+ eq(schema.connectorActionPolicies.connectionId, scope.connectionId),
16292
+ eq(schema.connectorActionPolicies.serverId, scope.serverId),
16293
+ eq(schema.connectorActionPolicies.toolName, scope.toolName),
16294
+ eq(schema.connectorActionPolicies.actionName, scope.actionName),
16295
+ ),
16296
+ )
16297
+ .for("update")
16298
+ .limit(1);
16299
+ if (existing?.policy === input.policy) return { policy: existing, changed: false };
16300
+ const now = new Date();
16301
+ const [row] = existing
16302
+ ? await tx
16303
+ .update(schema.connectorActionPolicies)
16304
+ .set({
16305
+ policy: input.policy,
16306
+ version: existing.version + 1,
16307
+ updatedBySubjectId: subjectId,
16308
+ updatedAt: now,
16309
+ })
16310
+ .where(eq(schema.connectorActionPolicies.id, existing.id))
16311
+ .returning()
16312
+ : await tx
16313
+ .insert(schema.connectorActionPolicies)
16314
+ .values({
16315
+ accountId: input.accountId,
16316
+ workspaceId: input.workspaceId,
16317
+ ...scope,
16318
+ policy: input.policy,
16319
+ createdBySubjectId: subjectId,
16320
+ updatedBySubjectId: subjectId,
16321
+ })
16322
+ .returning();
16323
+ if (!row) throw new Error("Failed to persist connector action policy");
16324
+ await tx.insert(schema.auditEvents).values({
16325
+ accountId: input.accountId,
16326
+ workspaceId: input.workspaceId,
16327
+ subjectId,
16328
+ action: "connector.action.policy_changed",
16329
+ targetType: "connector_action_policy",
16330
+ targetId: row.id,
16331
+ metadata: {
16332
+ connectionId: row.connectionId,
16333
+ serverId: row.serverId,
16334
+ toolName: row.toolName,
16335
+ actionName: row.actionName,
16336
+ policy: row.policy,
16337
+ version: row.version,
16338
+ previousPolicy: existing?.policy ?? null,
16339
+ previousVersion: existing?.version ?? null,
16340
+ },
16341
+ });
16342
+ return { policy: row, changed: true };
16343
+ }),
16344
+ );
16345
+ }
16346
+
16347
+ export async function prepareConnectorActionApproval(
16348
+ db: Database,
16349
+ identity: ConnectorActionAttemptIdentity,
16350
+ invocation: ConnectorActionInvocation,
16351
+ ): Promise<PrepareConnectorActionApprovalResult> {
16352
+ const normalized = normalizedConnectorActionInvocation(identity, invocation);
16353
+ if (!normalized.connectionId) {
16354
+ return { managed: false, decision: "unmanaged" };
16355
+ }
16356
+ return await withRlsContext(
16357
+ db,
16358
+ { accountId: identity.accountId, workspaceId: identity.workspaceId },
16359
+ async (scopedDb) =>
16360
+ await scopedDb.transaction(async (tx) => {
16361
+ const snapshot = await connectorActionAttemptSnapshot(tx as unknown as Database, identity);
16362
+ const resolved = resolveConnectorActionPolicy(snapshot, {
16363
+ connectionId: normalized.connectionId!,
16364
+ serverId: normalized.serverId,
16365
+ toolName: normalized.toolName,
16366
+ actionName: normalized.policyActionSelector,
16367
+ });
16368
+ if (!resolved.managed) return { managed: false, decision: "unmanaged" } as const;
16369
+ const durable = durableConnectorActionInvocation(
16370
+ identity,
16371
+ { ...normalized, connectionId: normalized.connectionId! },
16372
+ resolved,
16373
+ );
16374
+ const decision = resolved.entry?.policy ?? "block";
16375
+ if (decision === "allow") {
16376
+ return {
16377
+ managed: true,
16378
+ decision,
16379
+ actionFingerprint: durable.actionFingerprint,
16380
+ } as const;
16381
+ }
16382
+ const { row, inserted } = await insertConnectorActionRequest(tx as unknown as Database, {
16383
+ identity,
16384
+ invocation: durable,
16385
+ resolved,
16386
+ status: decision === "ask" ? "pending" : "blocked",
16387
+ });
16388
+ if (inserted) {
16389
+ await insertConnectorActionAudit(tx as unknown as Database, {
16390
+ row,
16391
+ action:
16392
+ decision === "ask"
16393
+ ? "connector.action.approval_requested"
16394
+ : "connector.action.blocked",
16395
+ subjectId: identity.initiator.subjectId,
16396
+ extra: { outcome: decision },
16397
+ });
16398
+ }
16399
+ return {
16400
+ managed: true,
16401
+ decision,
16402
+ requestId: row.id,
16403
+ actionFingerprint: row.actionFingerprint,
16404
+ } as const;
16405
+ }),
16406
+ );
16407
+ }
16408
+
16409
+ export async function beginConnectorActionExecution(
16410
+ db: Database,
16411
+ identity: ConnectorActionAttemptIdentity,
16412
+ invocation: ConnectorActionInvocation,
16413
+ ): Promise<BeginConnectorActionExecutionResult> {
16414
+ const normalized = normalizedConnectorActionInvocation(identity, invocation);
16415
+ if (!normalized.connectionId) {
16416
+ return { allowed: true, managed: false };
16417
+ }
16418
+ return await withRlsContext(
16419
+ db,
16420
+ { accountId: identity.accountId, workspaceId: identity.workspaceId },
16421
+ async (scopedDb) =>
16422
+ await scopedDb.transaction(async (tx) => {
16423
+ const snapshot = await connectorActionAttemptSnapshot(tx as unknown as Database, identity);
16424
+ const [existing] = await tx
16425
+ .select()
16426
+ .from(schema.connectorActionRequests)
16427
+ .where(
16428
+ and(
16429
+ eq(schema.connectorActionRequests.workspaceId, identity.workspaceId),
16430
+ eq(schema.connectorActionRequests.sessionId, identity.sessionId),
16431
+ eq(schema.connectorActionRequests.turnId, identity.turnId),
16432
+ eq(schema.connectorActionRequests.approvalId, normalized.approvalId),
16433
+ ),
16434
+ )
16435
+ .for("update")
16436
+ .limit(1);
16437
+ if (existing && !connectorActionRequestMatchesLogicalCall(existing, identity, normalized)) {
16438
+ throw new Error("connector action approval id conflicts with different immutable inputs");
16439
+ }
16440
+ let row = existing;
16441
+ let inserted = false;
16442
+ if (!row) {
16443
+ const resolved = resolveConnectorActionPolicy(snapshot, {
16444
+ connectionId: normalized.connectionId!,
16445
+ serverId: normalized.serverId,
16446
+ toolName: normalized.toolName,
16447
+ actionName: normalized.policyActionSelector,
16448
+ });
16449
+ if (!resolved.managed) return { allowed: true, managed: false } as const;
16450
+ const durable = durableConnectorActionInvocation(
16451
+ identity,
16452
+ { ...normalized, connectionId: normalized.connectionId! },
16453
+ resolved,
16454
+ );
16455
+ const decision = resolved.entry?.policy ?? "block";
16456
+ const created = await insertConnectorActionRequest(tx as unknown as Database, {
16457
+ identity,
16458
+ invocation: durable,
16459
+ resolved,
16460
+ status: decision === "ask" ? "pending" : decision === "block" ? "blocked" : "executing",
16461
+ });
16462
+ row = created.row;
16463
+ inserted = created.inserted;
16464
+ if (inserted) {
16465
+ await insertConnectorActionAudit(tx as unknown as Database, {
16466
+ row,
16467
+ action:
16468
+ decision === "ask"
16469
+ ? "connector.action.approval_requested"
16470
+ : decision === "block"
16471
+ ? "connector.action.blocked"
16472
+ : "connector.action.execution_started",
16473
+ subjectId: identity.initiator.subjectId,
16474
+ extra: { outcome: decision === "allow" ? "started" : decision },
16475
+ });
16476
+ }
16477
+ }
16478
+ if (row.status === "approved") {
16479
+ const [executing] = await tx
16480
+ .update(schema.connectorActionRequests)
16481
+ .set({
16482
+ status: "executing",
16483
+ executionAttemptId: identity.attemptId,
16484
+ executionAttemptGeneration: identity.executionGeneration,
16485
+ executionStartedAt: new Date(),
16486
+ updatedAt: new Date(),
16487
+ })
16488
+ .where(eq(schema.connectorActionRequests.id, row.id))
16489
+ .returning();
16490
+ if (!executing) throw new Error("Approved connector action request disappeared");
16491
+ row = executing;
16492
+ await insertConnectorActionAudit(tx as unknown as Database, {
16493
+ row,
16494
+ action: "connector.action.execution_started",
16495
+ subjectId: identity.initiator.subjectId,
16496
+ extra: { outcome: "started" },
16497
+ });
16498
+ return {
16499
+ allowed: true,
16500
+ managed: true,
16501
+ requestId: row.id,
16502
+ actionFingerprint: row.actionFingerprint,
16503
+ } as const;
16504
+ }
16505
+ if (row.status === "executing") {
16506
+ if (inserted) {
16507
+ return {
16508
+ allowed: true,
16509
+ managed: true,
16510
+ requestId: row.id,
16511
+ actionFingerprint: row.actionFingerprint,
16512
+ } as const;
16513
+ }
16514
+ const [uncertain] = await tx
16515
+ .update(schema.connectorActionRequests)
16516
+ .set({
16517
+ status: "uncertain",
16518
+ outcome: "retry_after_execution_started",
16519
+ executionFinishedAt: new Date(),
16520
+ updatedAt: new Date(),
16521
+ })
16522
+ .where(eq(schema.connectorActionRequests.id, row.id))
16523
+ .returning();
16524
+ if (!uncertain) throw new Error("Executing connector action request disappeared");
16525
+ await insertConnectorActionAudit(tx as unknown as Database, {
16526
+ row: uncertain,
16527
+ action: "connector.action.execution_uncertain",
16528
+ subjectId: identity.initiator.subjectId,
16529
+ extra: { outcome: "retry_denied" },
16530
+ });
16531
+ return {
16532
+ allowed: false,
16533
+ managed: true,
16534
+ reason: "uncertain_retry",
16535
+ requestId: uncertain.id,
16536
+ actionFingerprint: uncertain.actionFingerprint,
16537
+ } as const;
16538
+ }
16539
+ const reason =
16540
+ row.status === "pending"
16541
+ ? "approval_required"
16542
+ : row.status === "rejected"
16543
+ ? "rejected"
16544
+ : row.status === "blocked"
16545
+ ? "blocked"
16546
+ : "already_executed";
16547
+ return {
16548
+ allowed: false,
16549
+ managed: true,
16550
+ reason,
16551
+ requestId: row.id,
16552
+ actionFingerprint: row.actionFingerprint,
16553
+ } as const;
16554
+ }),
16555
+ );
16556
+ }
16557
+
16558
+ export async function completeConnectorActionExecution(
16559
+ db: Database,
16560
+ input: {
16561
+ accountId: string;
16562
+ workspaceId: string;
16563
+ requestId: string;
16564
+ attemptId: string;
16565
+ outcome: "completed" | "uncertain";
16566
+ },
16567
+ ): Promise<void> {
16568
+ await withRlsContext(
16569
+ db,
16570
+ { accountId: input.accountId, workspaceId: input.workspaceId },
16571
+ async (scopedDb) =>
16572
+ await scopedDb.transaction(async (tx) => {
16573
+ const [existing] = await tx
16574
+ .select()
16575
+ .from(schema.connectorActionRequests)
16576
+ .where(
16577
+ and(
16578
+ eq(schema.connectorActionRequests.workspaceId, input.workspaceId),
16579
+ eq(schema.connectorActionRequests.id, input.requestId),
16580
+ eq(schema.connectorActionRequests.executionAttemptId, input.attemptId),
16581
+ ),
16582
+ )
16583
+ .for("update")
16584
+ .limit(1);
16585
+ if (!existing) throw new Error("Connector action request not found for completion");
16586
+ if (existing.status === input.outcome) return;
16587
+ if (existing.status !== "executing") {
16588
+ throw new Error(`Connector action request cannot complete from ${existing.status}`);
16589
+ }
16590
+ const [row] = await tx
16591
+ .update(schema.connectorActionRequests)
16592
+ .set({
16593
+ status: input.outcome,
16594
+ outcome: input.outcome,
16595
+ executionFinishedAt: new Date(),
16596
+ updatedAt: new Date(),
16597
+ })
16598
+ .where(eq(schema.connectorActionRequests.id, existing.id))
16599
+ .returning();
16600
+ if (!row) throw new Error("Connector action request disappeared during completion");
16601
+ await insertConnectorActionAudit(tx as unknown as Database, {
16602
+ row,
16603
+ action:
16604
+ input.outcome === "completed"
16605
+ ? "connector.action.execution_completed"
16606
+ : "connector.action.execution_uncertain",
16607
+ subjectId: row.initiatorSubjectId,
16608
+ extra: { outcome: input.outcome },
16609
+ });
16610
+ }),
16611
+ );
16612
+ }
16613
+
15749
16614
  type DeploymentDepthPolicy = NestedAgentDepthDeploymentPolicy;
15750
16615
 
15751
16616
  /** Read the persisted deployment fallback; process configuration is not policy authority. */
@@ -36086,6 +36951,29 @@ export async function claimSessionWorkForAttempt(
36086
36951
  const mcpApprovalPolicies: Record<string, SessionMcpApprovalPolicy> = Object.fromEntries(
36087
36952
  policyRows.map((row) => [row.serverId, row.requireApproval ?? false]),
36088
36953
  );
36954
+ const connectorPolicyRows = await tx
36955
+ .select({
36956
+ id: schema.connectorActionPolicies.id,
36957
+ connectionId: schema.connectorActionPolicies.connectionId,
36958
+ serverId: schema.connectorActionPolicies.serverId,
36959
+ toolName: schema.connectorActionPolicies.toolName,
36960
+ actionName: schema.connectorActionPolicies.actionName,
36961
+ policy: schema.connectorActionPolicies.policy,
36962
+ version: schema.connectorActionPolicies.version,
36963
+ })
36964
+ .from(schema.connectorActionPolicies)
36965
+ .where(eq(schema.connectorActionPolicies.workspaceId, workspaceId))
36966
+ .orderBy(
36967
+ asc(schema.connectorActionPolicies.connectionId),
36968
+ asc(schema.connectorActionPolicies.serverId),
36969
+ asc(schema.connectorActionPolicies.toolName),
36970
+ asc(schema.connectorActionPolicies.actionName),
36971
+ asc(schema.connectorActionPolicies.id),
36972
+ )
36973
+ .limit(2049);
36974
+ if (connectorPolicyRows.length > 2048) {
36975
+ throw new Error("Connector action policy snapshot exceeds the 2048-row bound");
36976
+ }
36089
36977
  return await registerSessionTurnAttemptClaim(tx as unknown as Database, {
36090
36978
  id: input.attemptId,
36091
36979
  accountId: session.accountId,
@@ -36098,6 +36986,7 @@ export async function claimSessionWorkForAttempt(
36098
36986
  temporalActivityId: input.dispatchId,
36099
36987
  verifiedControlRevision: Number(workspaceControl.revision),
36100
36988
  mcpApprovalPolicies,
36989
+ connectorActionPolicies: connectorPolicyRows,
36101
36990
  });
36102
36991
  };
36103
36992
  if (session.activeTurnId !== null) {
@@ -41383,6 +42272,7 @@ export async function acceptSessionApprovalDecision(
41383
42272
  accountId: string;
41384
42273
  workspaceId: string;
41385
42274
  sessionId: string;
42275
+ subjectId: string;
41386
42276
  payload: Record<string, unknown>;
41387
42277
  clientEventId?: string | null;
41388
42278
  },
@@ -41521,6 +42411,41 @@ export async function acceptSessionApprovalDecision(
41521
42411
  })
41522
42412
  .returning();
41523
42413
  if (!event) throw new Error("Failed to append approval decision");
42414
+ const approvalDecision = input.payload.decision;
42415
+ if (approvalDecision === "approve" || approvalDecision === "reject") {
42416
+ const [connectorRequest] = await tx
42417
+ .update(schema.connectorActionRequests)
42418
+ .set({
42419
+ status: approvalDecision === "approve" ? "approved" : "rejected",
42420
+ decision: approvalDecision,
42421
+ decisionBySubjectId: boundedConnectorActionText(
42422
+ input.subjectId,
42423
+ "approval actor",
42424
+ 1024,
42425
+ ),
42426
+ decisionEventId: event.id,
42427
+ decidedAt: event.occurredAt,
42428
+ updatedAt: new Date(),
42429
+ })
42430
+ .where(
42431
+ and(
42432
+ eq(schema.connectorActionRequests.workspaceId, input.workspaceId),
42433
+ eq(schema.connectorActionRequests.sessionId, input.sessionId),
42434
+ eq(schema.connectorActionRequests.turnId, turn.id),
42435
+ eq(schema.connectorActionRequests.approvalId, approvalId),
42436
+ eq(schema.connectorActionRequests.status, "pending"),
42437
+ ),
42438
+ )
42439
+ .returning();
42440
+ if (connectorRequest) {
42441
+ await insertConnectorActionAudit(tx as unknown as Database, {
42442
+ row: connectorRequest,
42443
+ action: "connector.action.approval_decided",
42444
+ subjectId: input.subjectId,
42445
+ extra: { decision: approvalDecision },
42446
+ });
42447
+ }
42448
+ }
41524
42449
  await tx
41525
42450
  .update(schema.sessions)
41526
42451
  .set({