@opengeni/db 0.19.0 → 0.22.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
@@ -33,6 +33,7 @@ import type {
33
33
  HostUsageExport,
34
34
  HostUsageExportBatch,
35
35
  ManagedAccount,
36
+ McpPersonalConnectionDelegation,
36
37
  Permission,
37
38
  PackInstallation,
38
39
  PackInstallationStatus,
@@ -103,6 +104,7 @@ import type {
103
104
  } from "@opengeni/contracts";
104
105
  import {
105
106
  DEFAULT_FIRST_PARTY_MCP_TOOLS,
107
+ McpPersonalConnectionDelegations,
106
108
  SESSION_AUTHORIZATION_LIST_SCOPE_MAX_IDS,
107
109
  backendForNativeSnapshotProvider,
108
110
  canonicalModalCheckpointProviderBinding,
@@ -110,6 +112,7 @@ import {
110
112
  parseWorkspaceArchiveDescriptor,
111
113
  stableJson,
112
114
  } from "@opengeni/contracts";
115
+
113
116
  import {
114
117
  approvalIdentifier,
115
118
  boundWorkspaceControlEvent,
@@ -239,6 +242,7 @@ export * from "./new-session-drafts";
239
242
  export * from "./workspace-instruction-policies";
240
243
  export * from "./preference-registry";
241
244
  export * from "./memory-governance";
245
+ export * from "./scoped-knowledge";
242
246
  export { interruptedToolCallResult } from "./session-tool-call-settlement";
243
247
  export { decryptEnvironmentValue, encryptEnvironmentValue } from "./environment-crypto";
244
248
  export {
@@ -286,6 +290,17 @@ export * from "./memory-domain";
286
290
  // `userLookup` is the injection seam for hosts on a different driver.
287
291
  // `PostgresJsDatabase<typeof schema>` is assignable to this, so standalone is
288
292
  // unaffected.
293
+ function parsedPersonalConnectionDelegations(
294
+ value: unknown,
295
+ context: string,
296
+ ): McpPersonalConnectionDelegation[] {
297
+ const parsed = McpPersonalConnectionDelegations.safeParse(value);
298
+ if (!parsed.success) {
299
+ throw new Error(`Invalid personal MCP delegation snapshot at ${context}`);
300
+ }
301
+ return parsed.data.map((delegation) => ({ ...delegation }));
302
+ }
303
+
289
304
  export type Database = PgDatabase<any, typeof schema>;
290
305
 
291
306
  /** Raised when a durable session tool-policy write lost its version fence. */
@@ -3296,6 +3311,10 @@ export type CreateScheduledTaskInput = {
3296
3311
  runMode: ScheduledTaskRunMode;
3297
3312
  overlapPolicy: ScheduledTaskOverlapPolicy;
3298
3313
  agentConfig: ScheduledTaskAgentConfig;
3314
+ createdBy?: TurnInitiator;
3315
+ createdByContext?: TurnInitiatorContext;
3316
+ createdByActor?: AgentSessionCreationActor | null;
3317
+ personalConnectionDelegations?: McpPersonalConnectionDelegation[];
3299
3318
  variableSetId?: string | null;
3300
3319
  // The rig each run binds to (M3); active version resolved per fire at dispatch.
3301
3320
  rigId?: string | null;
@@ -3309,6 +3328,7 @@ export type UpdateScheduledTaskInput = Partial<{
3309
3328
  runMode: ScheduledTaskRunMode;
3310
3329
  overlapPolicy: ScheduledTaskOverlapPolicy;
3311
3330
  agentConfig: ScheduledTaskAgentConfig;
3331
+ personalConnectionDelegations: McpPersonalConnectionDelegation[];
3312
3332
  reusableSessionId: string | null;
3313
3333
  variableSetId: string | null;
3314
3334
  rigId: string | null;
@@ -3732,6 +3752,7 @@ export type EnqueueSessionTurnInput = {
3732
3752
  lineage?: Record<string, unknown>;
3733
3753
  initiator: TurnInitiator;
3734
3754
  initiatorContext?: TurnInitiatorContext;
3755
+ personalConnectionDelegations?: McpPersonalConnectionDelegation[];
3735
3756
  /** Steer inserts before all waiting prompts; Send appends after them. */
3736
3757
  placement?: "head" | "tail";
3737
3758
  };
@@ -3742,6 +3763,7 @@ export type EnqueueSessionTurnInput = {
3742
3763
  */
3743
3764
  export type SessionTurnForExecution = SessionTurn & {
3744
3765
  turnInstructions: string | null;
3766
+ personalConnectionDelegations: McpPersonalConnectionDelegation[];
3745
3767
  };
3746
3768
 
3747
3769
  export async function createFileUpload(
@@ -7144,7 +7166,9 @@ export async function loadConnectionCredentialForBroker(
7144
7166
  if (input.connectionId) {
7145
7167
  conditions.push(eq(schema.connections.id, input.connectionId));
7146
7168
  } else {
7147
- conditions.push(eq(schema.connections.providerDomain, input.providerDomain));
7169
+ conditions.push(
7170
+ sql`lower(${schema.connections.providerDomain}) = lower(${input.providerDomain})`,
7171
+ );
7148
7172
  if (input.kind) {
7149
7173
  conditions.push(eq(schema.connections.kind, input.kind));
7150
7174
  }
@@ -7174,6 +7198,8 @@ export async function loadConnectionCredentialForBroker(
7174
7198
  : [
7175
7199
  desc(sql`(${schema.connections.status} = 'active')`),
7176
7200
  desc(schema.connections.updatedAt),
7201
+ desc(schema.connections.createdAt),
7202
+ desc(schema.connections.id),
7177
7203
  ]),
7178
7204
  )
7179
7205
  .limit(1);
@@ -9018,7 +9044,27 @@ export async function createScheduledTask(
9018
9044
  db,
9019
9045
  { accountId: input.accountId, workspaceId: input.workspaceId },
9020
9046
  async (scopedDb) => {
9021
- const [row] = await scopedDb.insert(schema.scheduledTasks).values(input).returning();
9047
+ const frozenCreator = await frozenSessionCreatorForInsert(scopedDb, input);
9048
+ const [row] = await scopedDb
9049
+ .insert(schema.scheduledTasks)
9050
+ .values({
9051
+ id: input.id,
9052
+ accountId: input.accountId,
9053
+ workspaceId: input.workspaceId,
9054
+ name: input.name,
9055
+ status: input.status,
9056
+ schedule: input.schedule,
9057
+ temporalScheduleId: input.temporalScheduleId,
9058
+ runMode: input.runMode,
9059
+ overlapPolicy: input.overlapPolicy,
9060
+ agentConfig: input.agentConfig,
9061
+ ...creatorColumns(frozenCreator),
9062
+ personalConnectionDelegations: input.personalConnectionDelegations ?? [],
9063
+ variableSetId: input.variableSetId ?? null,
9064
+ rigId: input.rigId ?? null,
9065
+ metadata: input.metadata,
9066
+ })
9067
+ .returning();
9022
9068
  if (!row) {
9023
9069
  throw new Error("Failed to create scheduled task");
9024
9070
  }
@@ -9043,6 +9089,9 @@ export async function updateScheduledTask(
9043
9089
  ...(input.runMode !== undefined ? { runMode: input.runMode } : {}),
9044
9090
  ...(input.overlapPolicy !== undefined ? { overlapPolicy: input.overlapPolicy } : {}),
9045
9091
  ...(input.agentConfig !== undefined ? { agentConfig: input.agentConfig } : {}),
9092
+ ...(input.personalConnectionDelegations !== undefined
9093
+ ? { personalConnectionDelegations: input.personalConnectionDelegations }
9094
+ : {}),
9046
9095
  ...(input.reusableSessionId !== undefined
9047
9096
  ? { reusableSessionId: input.reusableSessionId }
9048
9097
  : {}),
@@ -9085,6 +9134,31 @@ export async function getScheduledTask(
9085
9134
  });
9086
9135
  }
9087
9136
 
9137
+ export async function getScheduledTaskPersonalConnectionDelegations(
9138
+ db: Database,
9139
+ workspaceId: string,
9140
+ taskId: string,
9141
+ ): Promise<McpPersonalConnectionDelegation[]> {
9142
+ return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
9143
+ const [row] = await scopedDb
9144
+ .select({ delegations: schema.scheduledTasks.personalConnectionDelegations })
9145
+ .from(schema.scheduledTasks)
9146
+ .where(
9147
+ and(
9148
+ eq(schema.scheduledTasks.workspaceId, workspaceId),
9149
+ eq(schema.scheduledTasks.id, taskId),
9150
+ ),
9151
+ )
9152
+ .limit(1);
9153
+ return row
9154
+ ? parsedPersonalConnectionDelegations(
9155
+ row.delegations,
9156
+ `scheduled_tasks:${workspaceId}:${taskId}`,
9157
+ )
9158
+ : [];
9159
+ });
9160
+ }
9161
+
9088
9162
  export async function requireScheduledTask(
9089
9163
  db: Database,
9090
9164
  workspaceId: string,
@@ -15672,6 +15746,816 @@ export async function listSessionMcpServersForRun(
15672
15746
  });
15673
15747
  }
15674
15748
 
15749
+ export type ConnectorActionPolicyDecision = schema.ConnectorActionPolicyDecision;
15750
+ export type ConnectorActionPolicySnapshotEntry = schema.ConnectorActionPolicySnapshotEntry;
15751
+
15752
+ export type ConnectorActionAttemptIdentity = {
15753
+ accountId: string;
15754
+ workspaceId: string;
15755
+ sessionId: string;
15756
+ turnId: string;
15757
+ attemptId: string;
15758
+ executionGeneration: number;
15759
+ initiator: Pick<TurnInitiator, "kind" | "subjectId">;
15760
+ };
15761
+
15762
+ export type ConnectorActionInvocation = {
15763
+ approvalId: string;
15764
+ connectionId?: string | null;
15765
+ serverId: string;
15766
+ toolName: string;
15767
+ arguments: unknown;
15768
+ };
15769
+
15770
+ export type PrepareConnectorActionApprovalResult =
15771
+ | { managed: false; decision: "unmanaged" }
15772
+ | {
15773
+ managed: true;
15774
+ decision: ConnectorActionPolicyDecision;
15775
+ requestId?: string;
15776
+ actionFingerprint: string;
15777
+ };
15778
+
15779
+ export type BeginConnectorActionExecutionResult =
15780
+ | { allowed: true; managed: false }
15781
+ | {
15782
+ allowed: true;
15783
+ managed: true;
15784
+ requestId: string;
15785
+ actionFingerprint: string;
15786
+ }
15787
+ | {
15788
+ allowed: false;
15789
+ managed: true;
15790
+ reason: "approval_required" | "blocked" | "rejected" | "already_executed" | "uncertain_retry";
15791
+ requestId: string;
15792
+ actionFingerprint: string;
15793
+ };
15794
+
15795
+ const CONNECTOR_ACTION_POLICY_SNAPSHOT_MAX = 2048;
15796
+ const CONNECTOR_ACTION_APPROVAL_ID_MAX = 1024;
15797
+ const CONNECTOR_ACTION_CONNECTION_ID_MAX = 512;
15798
+ const CONNECTOR_ACTION_SERVER_ID_MAX = 256;
15799
+ const CONNECTOR_ACTION_NAME_MAX = 512;
15800
+
15801
+ function boundedConnectorActionText(value: string, label: string, max: number): string {
15802
+ const trimmed = value.trim();
15803
+ if (trimmed.length === 0 || Buffer.byteLength(trimmed, "utf8") > max) {
15804
+ throw new Error(`${label} must be between 1 and ${max} UTF-8 bytes`);
15805
+ }
15806
+ return trimmed;
15807
+ }
15808
+
15809
+ /**
15810
+ * Resolve the request's policy selector. This caller-controlled value is used
15811
+ * only transiently to match the attempt-frozen policy snapshot; it must never
15812
+ * be copied into a request row or audit event.
15813
+ */
15814
+ function connectorActionPolicySelector(toolName: string, args: unknown): string {
15815
+ if (args && typeof args === "object" && !Array.isArray(args)) {
15816
+ const action = (args as Record<string, unknown>).action;
15817
+ if (typeof action === "string" && action.trim().length > 0) {
15818
+ return boundedConnectorActionText(action, "connector action name", CONNECTOR_ACTION_NAME_MAX);
15819
+ }
15820
+ }
15821
+ return toolName;
15822
+ }
15823
+
15824
+ function connectorActionEvidenceName(
15825
+ resolved: Exclude<ResolvedConnectorActionPolicy, { managed: false }>,
15826
+ ): string {
15827
+ return resolved.entry?.actionName ?? "*";
15828
+ }
15829
+
15830
+ function connectorActionFingerprint(input: {
15831
+ workspaceId: string;
15832
+ connectionId: string;
15833
+ serverId: string;
15834
+ toolName: string;
15835
+ actionName: string;
15836
+ arguments: unknown;
15837
+ }): string {
15838
+ return createHash("sha256")
15839
+ .update(
15840
+ stableJson({
15841
+ workspaceId: input.workspaceId,
15842
+ connectionId: input.connectionId,
15843
+ serverId: input.serverId,
15844
+ toolName: input.toolName,
15845
+ actionName: input.actionName,
15846
+ arguments: input.arguments ?? null,
15847
+ }),
15848
+ "utf8",
15849
+ )
15850
+ .digest("hex");
15851
+ }
15852
+
15853
+ type ResolvedConnectorActionPolicy =
15854
+ | { managed: false }
15855
+ | {
15856
+ managed: true;
15857
+ source: "explicit";
15858
+ entry: ConnectorActionPolicySnapshotEntry;
15859
+ }
15860
+ | {
15861
+ managed: true;
15862
+ source: "ambiguous";
15863
+ entry: null;
15864
+ decision: "block";
15865
+ };
15866
+
15867
+ /** Resolve one immutable attempt snapshot with exact-over-wildcard precedence. */
15868
+ export function resolveConnectorActionPolicy(
15869
+ snapshot: readonly ConnectorActionPolicySnapshotEntry[],
15870
+ input: { connectionId: string; serverId: string; toolName: string; actionName: string },
15871
+ ): ResolvedConnectorActionPolicy {
15872
+ const candidates = snapshot
15873
+ .filter(
15874
+ (entry) =>
15875
+ entry.connectionId === input.connectionId &&
15876
+ (entry.serverId === input.serverId || entry.serverId === "*") &&
15877
+ (entry.toolName === input.toolName || entry.toolName === "*") &&
15878
+ (entry.actionName === input.actionName || entry.actionName === "*"),
15879
+ )
15880
+ .map((entry) => ({
15881
+ entry,
15882
+ specificity:
15883
+ Number(entry.serverId !== "*") +
15884
+ Number(entry.toolName !== "*") +
15885
+ Number(entry.actionName !== "*"),
15886
+ }))
15887
+ .sort(
15888
+ (left, right) =>
15889
+ right.specificity - left.specificity || left.entry.id.localeCompare(right.entry.id),
15890
+ );
15891
+ const selected = candidates[0];
15892
+ if (!selected) return { managed: false };
15893
+ if (candidates[1]?.specificity === selected.specificity) {
15894
+ return { managed: true, source: "ambiguous", entry: null, decision: "block" };
15895
+ }
15896
+ return { managed: true, source: "explicit", entry: selected.entry };
15897
+ }
15898
+
15899
+ function connectorActionAuditMetadata(
15900
+ row: typeof schema.connectorActionRequests.$inferSelect,
15901
+ extra: Record<string, unknown> = {},
15902
+ ): Record<string, unknown> {
15903
+ return {
15904
+ requestId: row.id,
15905
+ sessionId: row.sessionId,
15906
+ turnId: row.turnId,
15907
+ attemptId: row.creationAttemptId,
15908
+ creationExecutionGeneration: row.creationExecutionGeneration,
15909
+ executionAttemptId: row.executionAttemptId,
15910
+ executionAttemptGeneration: row.executionAttemptGeneration,
15911
+ approvalId: row.approvalId,
15912
+ initiatorKind: row.initiatorKind,
15913
+ initiatorSubjectId: row.initiatorSubjectId,
15914
+ connectionId: row.connectionId,
15915
+ connectionVersion: row.connectionVersion,
15916
+ serverId: row.serverId,
15917
+ toolName: row.toolName,
15918
+ actionName: row.actionName,
15919
+ policyId: row.policyId,
15920
+ policyVersion: row.policyVersion,
15921
+ policySource: row.policySource,
15922
+ policyDecision: row.policyDecision,
15923
+ actionFingerprint: row.actionFingerprint,
15924
+ ...extra,
15925
+ };
15926
+ }
15927
+
15928
+ async function insertConnectorActionAudit(
15929
+ db: Database,
15930
+ input: {
15931
+ row: typeof schema.connectorActionRequests.$inferSelect;
15932
+ action: string;
15933
+ subjectId: string;
15934
+ extra?: Record<string, unknown>;
15935
+ },
15936
+ ): Promise<void> {
15937
+ await db.insert(schema.auditEvents).values({
15938
+ accountId: input.row.accountId,
15939
+ workspaceId: input.row.workspaceId,
15940
+ subjectId: input.subjectId,
15941
+ action: input.action,
15942
+ targetType: "connector_action_request",
15943
+ targetId: input.row.id,
15944
+ metadata: connectorActionAuditMetadata(input.row, input.extra),
15945
+ });
15946
+ }
15947
+
15948
+ function normalizedConnectorActionInvocation(
15949
+ identity: ConnectorActionAttemptIdentity,
15950
+ invocation: ConnectorActionInvocation,
15951
+ ): {
15952
+ approvalId: string;
15953
+ connectionId: string | null;
15954
+ serverId: string;
15955
+ toolName: string;
15956
+ policyActionSelector: string;
15957
+ arguments: unknown;
15958
+ } {
15959
+ const approvalId = boundedConnectorActionText(
15960
+ invocation.approvalId,
15961
+ "connector approval id",
15962
+ CONNECTOR_ACTION_APPROVAL_ID_MAX,
15963
+ );
15964
+ const serverId = boundedConnectorActionText(
15965
+ invocation.serverId,
15966
+ "connector server id",
15967
+ CONNECTOR_ACTION_SERVER_ID_MAX,
15968
+ );
15969
+ const toolName = boundedConnectorActionText(
15970
+ invocation.toolName,
15971
+ "connector tool name",
15972
+ CONNECTOR_ACTION_NAME_MAX,
15973
+ );
15974
+ const policyActionSelector = connectorActionPolicySelector(toolName, invocation.arguments);
15975
+ const connectionId = invocation.connectionId?.trim()
15976
+ ? boundedConnectorActionText(
15977
+ invocation.connectionId,
15978
+ "connector connection id",
15979
+ CONNECTOR_ACTION_CONNECTION_ID_MAX,
15980
+ )
15981
+ : null;
15982
+ return {
15983
+ approvalId,
15984
+ connectionId,
15985
+ serverId,
15986
+ toolName,
15987
+ policyActionSelector,
15988
+ arguments: invocation.arguments,
15989
+ };
15990
+ }
15991
+
15992
+ function durableConnectorActionInvocation(
15993
+ identity: ConnectorActionAttemptIdentity,
15994
+ invocation: ReturnType<typeof normalizedConnectorActionInvocation> & { connectionId: string },
15995
+ resolved: Exclude<ResolvedConnectorActionPolicy, { managed: false }>,
15996
+ ): {
15997
+ approvalId: string;
15998
+ connectionId: string;
15999
+ serverId: string;
16000
+ toolName: string;
16001
+ actionName: string;
16002
+ actionFingerprint: string;
16003
+ } {
16004
+ const actionName = connectorActionEvidenceName(resolved);
16005
+ return {
16006
+ approvalId: invocation.approvalId,
16007
+ connectionId: invocation.connectionId,
16008
+ serverId: invocation.serverId,
16009
+ toolName: invocation.toolName,
16010
+ actionName,
16011
+ actionFingerprint: connectorActionFingerprint({
16012
+ workspaceId: identity.workspaceId,
16013
+ connectionId: invocation.connectionId,
16014
+ serverId: invocation.serverId,
16015
+ toolName: invocation.toolName,
16016
+ actionName,
16017
+ arguments: invocation.arguments,
16018
+ }),
16019
+ };
16020
+ }
16021
+
16022
+ async function connectorActionAttemptSnapshot(
16023
+ db: Database,
16024
+ identity: ConnectorActionAttemptIdentity,
16025
+ ): Promise<ConnectorActionPolicySnapshotEntry[]> {
16026
+ const [attempt] = await db
16027
+ .select({
16028
+ accountId: schema.sessionTurnAttempts.accountId,
16029
+ sessionId: schema.sessionTurnAttempts.sessionId,
16030
+ turnId: schema.sessionTurnAttempts.turnId,
16031
+ executionGeneration: schema.sessionTurnAttempts.executionGeneration,
16032
+ state: schema.sessionTurnAttempts.state,
16033
+ connectorActionPolicies: schema.sessionTurnAttempts.connectorActionPolicies,
16034
+ })
16035
+ .from(schema.sessionTurnAttempts)
16036
+ .where(
16037
+ and(
16038
+ eq(schema.sessionTurnAttempts.workspaceId, identity.workspaceId),
16039
+ eq(schema.sessionTurnAttempts.id, identity.attemptId),
16040
+ ),
16041
+ )
16042
+ .limit(1);
16043
+ if (
16044
+ !attempt ||
16045
+ attempt.accountId !== identity.accountId ||
16046
+ attempt.sessionId !== identity.sessionId ||
16047
+ attempt.turnId !== identity.turnId ||
16048
+ attempt.executionGeneration !== identity.executionGeneration ||
16049
+ !["claimed", "running"].includes(attempt.state)
16050
+ ) {
16051
+ throw new Error(`connector action attempt ownership is unavailable: ${identity.attemptId}`);
16052
+ }
16053
+ if (attempt.connectorActionPolicies.length > CONNECTOR_ACTION_POLICY_SNAPSHOT_MAX) {
16054
+ throw new Error("connector action policy snapshot exceeds the runtime bound");
16055
+ }
16056
+ return attempt.connectorActionPolicies;
16057
+ }
16058
+
16059
+ function connectorActionRequestMatches(
16060
+ row: typeof schema.connectorActionRequests.$inferSelect,
16061
+ input: {
16062
+ identity: ConnectorActionAttemptIdentity;
16063
+ invocation: ReturnType<typeof durableConnectorActionInvocation>;
16064
+ resolved: Exclude<ResolvedConnectorActionPolicy, { managed: false }>;
16065
+ },
16066
+ ): boolean {
16067
+ const entry = input.resolved.entry;
16068
+ return (
16069
+ row.accountId === input.identity.accountId &&
16070
+ row.workspaceId === input.identity.workspaceId &&
16071
+ row.sessionId === input.identity.sessionId &&
16072
+ row.turnId === input.identity.turnId &&
16073
+ row.creationAttemptId === input.identity.attemptId &&
16074
+ row.creationExecutionGeneration === input.identity.executionGeneration &&
16075
+ row.approvalId === input.invocation.approvalId &&
16076
+ row.initiatorKind === input.identity.initiator.kind &&
16077
+ row.initiatorSubjectId === input.identity.initiator.subjectId &&
16078
+ row.connectionId === input.invocation.connectionId &&
16079
+ row.serverId === input.invocation.serverId &&
16080
+ row.toolName === input.invocation.toolName &&
16081
+ row.actionName === input.invocation.actionName &&
16082
+ row.policyId === (entry?.id ?? null) &&
16083
+ row.policyVersion === (entry?.version ?? null) &&
16084
+ row.policySource === input.resolved.source &&
16085
+ row.policyDecision === (entry?.policy ?? "block") &&
16086
+ row.actionFingerprint === input.invocation.actionFingerprint
16087
+ );
16088
+ }
16089
+
16090
+ function connectorActionRequestMatchesLogicalCall(
16091
+ row: typeof schema.connectorActionRequests.$inferSelect,
16092
+ identity: ConnectorActionAttemptIdentity,
16093
+ invocation: ReturnType<typeof normalizedConnectorActionInvocation>,
16094
+ ): boolean {
16095
+ if (!invocation.connectionId) return false;
16096
+ return (
16097
+ row.accountId === identity.accountId &&
16098
+ row.workspaceId === identity.workspaceId &&
16099
+ row.sessionId === identity.sessionId &&
16100
+ row.turnId === identity.turnId &&
16101
+ row.approvalId === invocation.approvalId &&
16102
+ row.initiatorKind === identity.initiator.kind &&
16103
+ row.initiatorSubjectId === identity.initiator.subjectId &&
16104
+ row.connectionId === invocation.connectionId &&
16105
+ row.serverId === invocation.serverId &&
16106
+ row.toolName === invocation.toolName &&
16107
+ row.actionFingerprint ===
16108
+ connectorActionFingerprint({
16109
+ workspaceId: identity.workspaceId,
16110
+ connectionId: invocation.connectionId,
16111
+ serverId: invocation.serverId,
16112
+ toolName: invocation.toolName,
16113
+ actionName: row.actionName,
16114
+ arguments: invocation.arguments,
16115
+ })
16116
+ );
16117
+ }
16118
+
16119
+ async function insertConnectorActionRequest(
16120
+ db: Database,
16121
+ input: {
16122
+ identity: ConnectorActionAttemptIdentity;
16123
+ invocation: ReturnType<typeof durableConnectorActionInvocation>;
16124
+ resolved: Exclude<ResolvedConnectorActionPolicy, { managed: false }>;
16125
+ status: "pending" | "blocked" | "executing";
16126
+ },
16127
+ ): Promise<{ row: typeof schema.connectorActionRequests.$inferSelect; inserted: boolean }> {
16128
+ const entry = input.resolved.entry;
16129
+ const [inserted] = await db
16130
+ .insert(schema.connectorActionRequests)
16131
+ .values({
16132
+ accountId: input.identity.accountId,
16133
+ workspaceId: input.identity.workspaceId,
16134
+ sessionId: input.identity.sessionId,
16135
+ turnId: input.identity.turnId,
16136
+ creationAttemptId: input.identity.attemptId,
16137
+ creationExecutionGeneration: input.identity.executionGeneration,
16138
+ approvalId: input.invocation.approvalId,
16139
+ initiatorKind: input.identity.initiator.kind,
16140
+ initiatorSubjectId: input.identity.initiator.subjectId,
16141
+ connectionId: input.invocation.connectionId!,
16142
+ serverId: input.invocation.serverId,
16143
+ toolName: input.invocation.toolName,
16144
+ actionName: input.invocation.actionName,
16145
+ policyId: entry?.id ?? null,
16146
+ policyVersion: entry?.version ?? null,
16147
+ policySource: input.resolved.source,
16148
+ policyDecision: entry?.policy ?? "block",
16149
+ actionFingerprint: input.invocation.actionFingerprint!,
16150
+ status: input.status,
16151
+ ...(input.status === "executing"
16152
+ ? {
16153
+ executionAttemptId: input.identity.attemptId,
16154
+ executionAttemptGeneration: input.identity.executionGeneration,
16155
+ executionStartedAt: new Date(),
16156
+ }
16157
+ : {}),
16158
+ })
16159
+ .onConflictDoNothing({
16160
+ target: [
16161
+ schema.connectorActionRequests.workspaceId,
16162
+ schema.connectorActionRequests.sessionId,
16163
+ schema.connectorActionRequests.turnId,
16164
+ schema.connectorActionRequests.approvalId,
16165
+ ],
16166
+ })
16167
+ .returning();
16168
+ if (inserted) return { row: inserted, inserted: true };
16169
+ const [existing] = await db
16170
+ .select()
16171
+ .from(schema.connectorActionRequests)
16172
+ .where(
16173
+ and(
16174
+ eq(schema.connectorActionRequests.workspaceId, input.identity.workspaceId),
16175
+ eq(schema.connectorActionRequests.sessionId, input.identity.sessionId),
16176
+ eq(schema.connectorActionRequests.turnId, input.identity.turnId),
16177
+ eq(schema.connectorActionRequests.approvalId, input.invocation.approvalId),
16178
+ ),
16179
+ )
16180
+ .for("update")
16181
+ .limit(1);
16182
+ if (!existing || !connectorActionRequestMatches(existing, input)) {
16183
+ throw new Error("connector action approval id conflicts with different immutable inputs");
16184
+ }
16185
+ return { row: existing, inserted: false };
16186
+ }
16187
+
16188
+ export async function upsertConnectorActionPolicy(
16189
+ db: Database,
16190
+ input: {
16191
+ accountId: string;
16192
+ workspaceId: string;
16193
+ subjectId: string;
16194
+ connectionId: string;
16195
+ serverId: string;
16196
+ toolName: string;
16197
+ actionName: string;
16198
+ policy: ConnectorActionPolicyDecision;
16199
+ },
16200
+ ): Promise<{ policy: typeof schema.connectorActionPolicies.$inferSelect; changed: boolean }> {
16201
+ const scope = {
16202
+ connectionId: boundedConnectorActionText(
16203
+ input.connectionId,
16204
+ "connector connection id",
16205
+ CONNECTOR_ACTION_CONNECTION_ID_MAX,
16206
+ ),
16207
+ serverId: boundedConnectorActionText(
16208
+ input.serverId,
16209
+ "connector server id",
16210
+ CONNECTOR_ACTION_SERVER_ID_MAX,
16211
+ ),
16212
+ toolName: boundedConnectorActionText(
16213
+ input.toolName,
16214
+ "connector tool name",
16215
+ CONNECTOR_ACTION_NAME_MAX,
16216
+ ),
16217
+ actionName: boundedConnectorActionText(
16218
+ input.actionName,
16219
+ "connector action name",
16220
+ CONNECTOR_ACTION_NAME_MAX,
16221
+ ),
16222
+ };
16223
+ const subjectId = boundedConnectorActionText(input.subjectId, "policy actor", 1024);
16224
+ return await withRlsContext(
16225
+ db,
16226
+ { accountId: input.accountId, workspaceId: input.workspaceId },
16227
+ async (scopedDb) =>
16228
+ await scopedDb.transaction(async (tx) => {
16229
+ await assertWorkspaceAccountPairInScope(tx, input.accountId, input.workspaceId);
16230
+ const [existing] = await tx
16231
+ .select()
16232
+ .from(schema.connectorActionPolicies)
16233
+ .where(
16234
+ and(
16235
+ eq(schema.connectorActionPolicies.workspaceId, input.workspaceId),
16236
+ eq(schema.connectorActionPolicies.connectionId, scope.connectionId),
16237
+ eq(schema.connectorActionPolicies.serverId, scope.serverId),
16238
+ eq(schema.connectorActionPolicies.toolName, scope.toolName),
16239
+ eq(schema.connectorActionPolicies.actionName, scope.actionName),
16240
+ ),
16241
+ )
16242
+ .for("update")
16243
+ .limit(1);
16244
+ if (existing?.policy === input.policy) return { policy: existing, changed: false };
16245
+ const now = new Date();
16246
+ const [row] = existing
16247
+ ? await tx
16248
+ .update(schema.connectorActionPolicies)
16249
+ .set({
16250
+ policy: input.policy,
16251
+ version: existing.version + 1,
16252
+ updatedBySubjectId: subjectId,
16253
+ updatedAt: now,
16254
+ })
16255
+ .where(eq(schema.connectorActionPolicies.id, existing.id))
16256
+ .returning()
16257
+ : await tx
16258
+ .insert(schema.connectorActionPolicies)
16259
+ .values({
16260
+ accountId: input.accountId,
16261
+ workspaceId: input.workspaceId,
16262
+ ...scope,
16263
+ policy: input.policy,
16264
+ createdBySubjectId: subjectId,
16265
+ updatedBySubjectId: subjectId,
16266
+ })
16267
+ .returning();
16268
+ if (!row) throw new Error("Failed to persist connector action policy");
16269
+ await tx.insert(schema.auditEvents).values({
16270
+ accountId: input.accountId,
16271
+ workspaceId: input.workspaceId,
16272
+ subjectId,
16273
+ action: "connector.action.policy_changed",
16274
+ targetType: "connector_action_policy",
16275
+ targetId: row.id,
16276
+ metadata: {
16277
+ connectionId: row.connectionId,
16278
+ serverId: row.serverId,
16279
+ toolName: row.toolName,
16280
+ actionName: row.actionName,
16281
+ policy: row.policy,
16282
+ version: row.version,
16283
+ previousPolicy: existing?.policy ?? null,
16284
+ previousVersion: existing?.version ?? null,
16285
+ },
16286
+ });
16287
+ return { policy: row, changed: true };
16288
+ }),
16289
+ );
16290
+ }
16291
+
16292
+ export async function prepareConnectorActionApproval(
16293
+ db: Database,
16294
+ identity: ConnectorActionAttemptIdentity,
16295
+ invocation: ConnectorActionInvocation,
16296
+ ): Promise<PrepareConnectorActionApprovalResult> {
16297
+ const normalized = normalizedConnectorActionInvocation(identity, invocation);
16298
+ if (!normalized.connectionId) {
16299
+ return { managed: false, decision: "unmanaged" };
16300
+ }
16301
+ return await withRlsContext(
16302
+ db,
16303
+ { accountId: identity.accountId, workspaceId: identity.workspaceId },
16304
+ async (scopedDb) =>
16305
+ await scopedDb.transaction(async (tx) => {
16306
+ const snapshot = await connectorActionAttemptSnapshot(tx as unknown as Database, identity);
16307
+ const resolved = resolveConnectorActionPolicy(snapshot, {
16308
+ connectionId: normalized.connectionId!,
16309
+ serverId: normalized.serverId,
16310
+ toolName: normalized.toolName,
16311
+ actionName: normalized.policyActionSelector,
16312
+ });
16313
+ if (!resolved.managed) return { managed: false, decision: "unmanaged" } as const;
16314
+ const durable = durableConnectorActionInvocation(
16315
+ identity,
16316
+ { ...normalized, connectionId: normalized.connectionId! },
16317
+ resolved,
16318
+ );
16319
+ const decision = resolved.entry?.policy ?? "block";
16320
+ if (decision === "allow") {
16321
+ return {
16322
+ managed: true,
16323
+ decision,
16324
+ actionFingerprint: durable.actionFingerprint,
16325
+ } as const;
16326
+ }
16327
+ const { row, inserted } = await insertConnectorActionRequest(tx as unknown as Database, {
16328
+ identity,
16329
+ invocation: durable,
16330
+ resolved,
16331
+ status: decision === "ask" ? "pending" : "blocked",
16332
+ });
16333
+ if (inserted) {
16334
+ await insertConnectorActionAudit(tx as unknown as Database, {
16335
+ row,
16336
+ action:
16337
+ decision === "ask"
16338
+ ? "connector.action.approval_requested"
16339
+ : "connector.action.blocked",
16340
+ subjectId: identity.initiator.subjectId,
16341
+ extra: { outcome: decision },
16342
+ });
16343
+ }
16344
+ return {
16345
+ managed: true,
16346
+ decision,
16347
+ requestId: row.id,
16348
+ actionFingerprint: row.actionFingerprint,
16349
+ } as const;
16350
+ }),
16351
+ );
16352
+ }
16353
+
16354
+ export async function beginConnectorActionExecution(
16355
+ db: Database,
16356
+ identity: ConnectorActionAttemptIdentity,
16357
+ invocation: ConnectorActionInvocation,
16358
+ ): Promise<BeginConnectorActionExecutionResult> {
16359
+ const normalized = normalizedConnectorActionInvocation(identity, invocation);
16360
+ if (!normalized.connectionId) {
16361
+ return { allowed: true, managed: false };
16362
+ }
16363
+ return await withRlsContext(
16364
+ db,
16365
+ { accountId: identity.accountId, workspaceId: identity.workspaceId },
16366
+ async (scopedDb) =>
16367
+ await scopedDb.transaction(async (tx) => {
16368
+ const snapshot = await connectorActionAttemptSnapshot(tx as unknown as Database, identity);
16369
+ const [existing] = await tx
16370
+ .select()
16371
+ .from(schema.connectorActionRequests)
16372
+ .where(
16373
+ and(
16374
+ eq(schema.connectorActionRequests.workspaceId, identity.workspaceId),
16375
+ eq(schema.connectorActionRequests.sessionId, identity.sessionId),
16376
+ eq(schema.connectorActionRequests.turnId, identity.turnId),
16377
+ eq(schema.connectorActionRequests.approvalId, normalized.approvalId),
16378
+ ),
16379
+ )
16380
+ .for("update")
16381
+ .limit(1);
16382
+ if (existing && !connectorActionRequestMatchesLogicalCall(existing, identity, normalized)) {
16383
+ throw new Error("connector action approval id conflicts with different immutable inputs");
16384
+ }
16385
+ let row = existing;
16386
+ let inserted = false;
16387
+ if (!row) {
16388
+ const resolved = resolveConnectorActionPolicy(snapshot, {
16389
+ connectionId: normalized.connectionId!,
16390
+ serverId: normalized.serverId,
16391
+ toolName: normalized.toolName,
16392
+ actionName: normalized.policyActionSelector,
16393
+ });
16394
+ if (!resolved.managed) return { allowed: true, managed: false } as const;
16395
+ const durable = durableConnectorActionInvocation(
16396
+ identity,
16397
+ { ...normalized, connectionId: normalized.connectionId! },
16398
+ resolved,
16399
+ );
16400
+ const decision = resolved.entry?.policy ?? "block";
16401
+ const created = await insertConnectorActionRequest(tx as unknown as Database, {
16402
+ identity,
16403
+ invocation: durable,
16404
+ resolved,
16405
+ status: decision === "ask" ? "pending" : decision === "block" ? "blocked" : "executing",
16406
+ });
16407
+ row = created.row;
16408
+ inserted = created.inserted;
16409
+ if (inserted) {
16410
+ await insertConnectorActionAudit(tx as unknown as Database, {
16411
+ row,
16412
+ action:
16413
+ decision === "ask"
16414
+ ? "connector.action.approval_requested"
16415
+ : decision === "block"
16416
+ ? "connector.action.blocked"
16417
+ : "connector.action.execution_started",
16418
+ subjectId: identity.initiator.subjectId,
16419
+ extra: { outcome: decision === "allow" ? "started" : decision },
16420
+ });
16421
+ }
16422
+ }
16423
+ if (row.status === "approved") {
16424
+ const [executing] = await tx
16425
+ .update(schema.connectorActionRequests)
16426
+ .set({
16427
+ status: "executing",
16428
+ executionAttemptId: identity.attemptId,
16429
+ executionAttemptGeneration: identity.executionGeneration,
16430
+ executionStartedAt: new Date(),
16431
+ updatedAt: new Date(),
16432
+ })
16433
+ .where(eq(schema.connectorActionRequests.id, row.id))
16434
+ .returning();
16435
+ if (!executing) throw new Error("Approved connector action request disappeared");
16436
+ row = executing;
16437
+ await insertConnectorActionAudit(tx as unknown as Database, {
16438
+ row,
16439
+ action: "connector.action.execution_started",
16440
+ subjectId: identity.initiator.subjectId,
16441
+ extra: { outcome: "started" },
16442
+ });
16443
+ return {
16444
+ allowed: true,
16445
+ managed: true,
16446
+ requestId: row.id,
16447
+ actionFingerprint: row.actionFingerprint,
16448
+ } as const;
16449
+ }
16450
+ if (row.status === "executing") {
16451
+ if (inserted) {
16452
+ return {
16453
+ allowed: true,
16454
+ managed: true,
16455
+ requestId: row.id,
16456
+ actionFingerprint: row.actionFingerprint,
16457
+ } as const;
16458
+ }
16459
+ const [uncertain] = await tx
16460
+ .update(schema.connectorActionRequests)
16461
+ .set({
16462
+ status: "uncertain",
16463
+ outcome: "retry_after_execution_started",
16464
+ executionFinishedAt: new Date(),
16465
+ updatedAt: new Date(),
16466
+ })
16467
+ .where(eq(schema.connectorActionRequests.id, row.id))
16468
+ .returning();
16469
+ if (!uncertain) throw new Error("Executing connector action request disappeared");
16470
+ await insertConnectorActionAudit(tx as unknown as Database, {
16471
+ row: uncertain,
16472
+ action: "connector.action.execution_uncertain",
16473
+ subjectId: identity.initiator.subjectId,
16474
+ extra: { outcome: "retry_denied" },
16475
+ });
16476
+ return {
16477
+ allowed: false,
16478
+ managed: true,
16479
+ reason: "uncertain_retry",
16480
+ requestId: uncertain.id,
16481
+ actionFingerprint: uncertain.actionFingerprint,
16482
+ } as const;
16483
+ }
16484
+ const reason =
16485
+ row.status === "pending"
16486
+ ? "approval_required"
16487
+ : row.status === "rejected"
16488
+ ? "rejected"
16489
+ : row.status === "blocked"
16490
+ ? "blocked"
16491
+ : "already_executed";
16492
+ return {
16493
+ allowed: false,
16494
+ managed: true,
16495
+ reason,
16496
+ requestId: row.id,
16497
+ actionFingerprint: row.actionFingerprint,
16498
+ } as const;
16499
+ }),
16500
+ );
16501
+ }
16502
+
16503
+ export async function completeConnectorActionExecution(
16504
+ db: Database,
16505
+ input: {
16506
+ accountId: string;
16507
+ workspaceId: string;
16508
+ requestId: string;
16509
+ attemptId: string;
16510
+ outcome: "completed" | "uncertain";
16511
+ },
16512
+ ): Promise<void> {
16513
+ await withRlsContext(
16514
+ db,
16515
+ { accountId: input.accountId, workspaceId: input.workspaceId },
16516
+ async (scopedDb) =>
16517
+ await scopedDb.transaction(async (tx) => {
16518
+ const [existing] = await tx
16519
+ .select()
16520
+ .from(schema.connectorActionRequests)
16521
+ .where(
16522
+ and(
16523
+ eq(schema.connectorActionRequests.workspaceId, input.workspaceId),
16524
+ eq(schema.connectorActionRequests.id, input.requestId),
16525
+ eq(schema.connectorActionRequests.executionAttemptId, input.attemptId),
16526
+ ),
16527
+ )
16528
+ .for("update")
16529
+ .limit(1);
16530
+ if (!existing) throw new Error("Connector action request not found for completion");
16531
+ if (existing.status === input.outcome) return;
16532
+ if (existing.status !== "executing") {
16533
+ throw new Error(`Connector action request cannot complete from ${existing.status}`);
16534
+ }
16535
+ const [row] = await tx
16536
+ .update(schema.connectorActionRequests)
16537
+ .set({
16538
+ status: input.outcome,
16539
+ outcome: input.outcome,
16540
+ executionFinishedAt: new Date(),
16541
+ updatedAt: new Date(),
16542
+ })
16543
+ .where(eq(schema.connectorActionRequests.id, existing.id))
16544
+ .returning();
16545
+ if (!row) throw new Error("Connector action request disappeared during completion");
16546
+ await insertConnectorActionAudit(tx as unknown as Database, {
16547
+ row,
16548
+ action:
16549
+ input.outcome === "completed"
16550
+ ? "connector.action.execution_completed"
16551
+ : "connector.action.execution_uncertain",
16552
+ subjectId: row.initiatorSubjectId,
16553
+ extra: { outcome: input.outcome },
16554
+ });
16555
+ }),
16556
+ );
16557
+ }
16558
+
15675
16559
  type DeploymentDepthPolicy = NestedAgentDepthDeploymentPolicy;
15676
16560
 
15677
16561
  /** Read the persisted deployment fallback; process configuration is not policy authority. */
@@ -15759,6 +16643,7 @@ export type SessionCreateInput = {
15759
16643
  sandboxGroupId?: string | null;
15760
16644
  sandboxOs?: SandboxOs;
15761
16645
  mcpServers?: CreateSessionMcpServerInput[];
16646
+ personalConnectionDelegations?: McpPersonalConnectionDelegation[];
15762
16647
  maxNestedAgentDepthOverride?: number | null;
15763
16648
  allowNestedAgentDepthIncrease?: boolean;
15764
16649
  subjectId?: string | null;
@@ -16131,6 +17016,11 @@ async function createSessionInTransaction(
16131
17016
 
16132
17017
  // Do not run mutable creator validation before keyed denial replay above.
16133
17018
  const frozenCreator = await frozenSessionCreatorForInsert(tx, input);
17019
+ const parentTurnId = input.parentSessionId
17020
+ ? input.createdByActor?.sessionId === input.parentSessionId
17021
+ ? input.createdByActor.turnId
17022
+ : null
17023
+ : null;
16134
17024
  const [inserted] = await tx
16135
17025
  .insert(schema.sessions)
16136
17026
  .values({
@@ -16157,8 +17047,10 @@ async function createSessionInTransaction(
16157
17047
  rigVersionId: input.rigVersionId ?? null,
16158
17048
  firstPartyMcpPermissions: input.firstPartyMcpPermissions ?? null,
16159
17049
  firstPartyMcpTools: input.firstPartyMcpTools ?? [...DEFAULT_FIRST_PARTY_MCP_TOOLS],
17050
+ initialPersonalConnectionDelegations: input.personalConnectionDelegations ?? [],
16160
17051
  instructions: input.instructions ?? null,
16161
17052
  parentSessionId: input.parentSessionId ?? null,
17053
+ parentTurnId,
16162
17054
  createIdempotencyKey,
16163
17055
  rootSessionId: decision.rootSessionId,
16164
17056
  nestedAgentDepth: decision.nestedAgentDepth,
@@ -16329,6 +17221,76 @@ export async function getSession(
16329
17221
  });
16330
17222
  }
16331
17223
 
17224
+ async function personalConnectionDelegationsForTurnInTransaction(
17225
+ db: Database,
17226
+ workspaceId: string,
17227
+ sessionId: string,
17228
+ turnId: string,
17229
+ ): Promise<McpPersonalConnectionDelegation[]> {
17230
+ const [row] = await db
17231
+ .select({ delegations: schema.sessionTurns.personalConnectionDelegations })
17232
+ .from(schema.sessionTurns)
17233
+ .where(
17234
+ and(
17235
+ eq(schema.sessionTurns.workspaceId, workspaceId),
17236
+ eq(schema.sessionTurns.sessionId, sessionId),
17237
+ eq(schema.sessionTurns.id, turnId),
17238
+ ),
17239
+ )
17240
+ .limit(1);
17241
+ return row
17242
+ ? parsedPersonalConnectionDelegations(
17243
+ row.delegations,
17244
+ `session_turns:${workspaceId}:${sessionId}:${turnId}`,
17245
+ )
17246
+ : [];
17247
+ }
17248
+
17249
+ export async function getSessionTurnPersonalConnectionDelegations(
17250
+ db: Database,
17251
+ workspaceId: string,
17252
+ sessionId: string,
17253
+ turnId: string,
17254
+ ): Promise<McpPersonalConnectionDelegation[]> {
17255
+ return await withWorkspaceRls(
17256
+ db,
17257
+ workspaceId,
17258
+ async (scopedDb) =>
17259
+ await personalConnectionDelegationsForTurnInTransaction(
17260
+ scopedDb,
17261
+ workspaceId,
17262
+ sessionId,
17263
+ turnId,
17264
+ ),
17265
+ );
17266
+ }
17267
+
17268
+ export async function getSessionParentPersonalConnectionDelegations(
17269
+ db: Database,
17270
+ workspaceId: string,
17271
+ childSessionId: string,
17272
+ ): Promise<McpPersonalConnectionDelegation[]> {
17273
+ return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
17274
+ const [child] = await scopedDb
17275
+ .select({
17276
+ parentSessionId: schema.sessions.parentSessionId,
17277
+ parentTurnId: schema.sessions.parentTurnId,
17278
+ })
17279
+ .from(schema.sessions)
17280
+ .where(
17281
+ and(eq(schema.sessions.workspaceId, workspaceId), eq(schema.sessions.id, childSessionId)),
17282
+ )
17283
+ .limit(1);
17284
+ if (!child?.parentSessionId || !child.parentTurnId) return [];
17285
+ return await personalConnectionDelegationsForTurnInTransaction(
17286
+ scopedDb,
17287
+ workspaceId,
17288
+ child.parentSessionId,
17289
+ child.parentTurnId,
17290
+ );
17291
+ });
17292
+ }
17293
+
16332
17294
  export async function getSessionSpawnDenial(
16333
17295
  db: Database,
16334
17296
  workspaceId: string,
@@ -17692,6 +18654,56 @@ export async function listSessions(
17692
18654
  });
17693
18655
  }
17694
18656
 
18657
+ /**
18658
+ * Return the model most recently chosen by one human subject in their own
18659
+ * workspace sessions. A later subject-initiated turn supersedes the session's
18660
+ * creation-time default; sessions with no such turn fall back to sessions.model.
18661
+ * Turn timestamp ties use queue position then UUID, and session ties use the
18662
+ * immutable session timestamp/UUID. Other subjects' turns never influence the
18663
+ * result. The workspace filter remains authoritative under FORCE RLS.
18664
+ */
18665
+ export async function getLatestSessionModelForSubject(
18666
+ db: Database,
18667
+ workspaceId: string,
18668
+ subjectId: string,
18669
+ ): Promise<string | null> {
18670
+ return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
18671
+ const rows = await scopedDb.execute<{ model: string }>(sql`
18672
+ select coalesce(latest_subject_turn.model, subject_session.model) as model
18673
+ from ${schema.sessions} subject_session
18674
+ left join lateral (
18675
+ select
18676
+ subject_turn.id,
18677
+ subject_turn.model,
18678
+ subject_turn.position,
18679
+ subject_turn.created_at
18680
+ from ${schema.sessionTurns} subject_turn
18681
+ where subject_turn.workspace_id = ${workspaceId}
18682
+ and subject_turn.session_id = subject_session.id
18683
+ and subject_turn.initiator_kind = 'subject'
18684
+ and subject_turn.initiator_subject_id = ${subjectId}
18685
+ order by
18686
+ subject_turn.created_at desc,
18687
+ subject_turn.position desc,
18688
+ subject_turn.id desc
18689
+ limit 1
18690
+ ) latest_subject_turn on true
18691
+ where subject_session.workspace_id = ${workspaceId}
18692
+ and subject_session.created_by_kind = 'subject'
18693
+ and subject_session.created_by_subject_id = ${subjectId}
18694
+ order by
18695
+ coalesce(latest_subject_turn.created_at, subject_session.created_at) desc,
18696
+ (latest_subject_turn.id is not null) desc,
18697
+ subject_session.created_at desc,
18698
+ subject_session.id desc,
18699
+ latest_subject_turn.position desc nulls last,
18700
+ latest_subject_turn.id desc nulls last
18701
+ limit 1
18702
+ `);
18703
+ return rows[0]?.model ?? null;
18704
+ });
18705
+ }
18706
+
17695
18707
  export type SessionDiscoveryOrderBy = "createdAt" | "updatedAt";
17696
18708
  export type SessionDiscoveryCursor = {
17697
18709
  orderBy: SessionDiscoveryOrderBy;
@@ -34812,6 +35824,32 @@ export async function materializeGoalContinuation(
34812
35824
  return { action: "paused", events: [mapEvent(event)] } as const;
34813
35825
  }
34814
35826
 
35827
+ const [causalTurn] = await tx
35828
+ .select({
35829
+ id: schema.sessionTurns.id,
35830
+ personalConnectionDelegations: schema.sessionTurns.personalConnectionDelegations,
35831
+ })
35832
+ .from(schema.sessionTurns)
35833
+ .where(
35834
+ and(
35835
+ eq(schema.sessionTurns.workspaceId, input.workspaceId),
35836
+ eq(schema.sessionTurns.sessionId, input.sessionId),
35837
+ sql`${schema.sessionTurns.finishedAt} is not null`,
35838
+ ),
35839
+ )
35840
+ .orderBy(
35841
+ desc(schema.sessionTurns.position),
35842
+ desc(schema.sessionTurns.createdAt),
35843
+ desc(schema.sessionTurns.id),
35844
+ )
35845
+ .limit(1);
35846
+ const personalConnectionDelegations = causalTurn
35847
+ ? parsedPersonalConnectionDelegations(
35848
+ causalTurn.personalConnectionDelegations,
35849
+ `session_turns:${input.workspaceId}:${input.sessionId}:${causalTurn.id}`,
35850
+ )
35851
+ : [];
35852
+
34815
35853
  const prompt = input.prompt(decision.goal, decision.autoContinuation, decision.cap);
34816
35854
  const payload = {
34817
35855
  type: "goal_continuation" as const,
@@ -34841,7 +35879,12 @@ export async function materializeGoalContinuation(
34841
35879
  dedupeKey: `goal-continuation:${decision.goal.id}:wake:${goalWakeRevision}`,
34842
35880
  summary: prompt,
34843
35881
  payload,
34844
- lineage: { goalId: decision.goal.id, goalWakeRevision },
35882
+ lineage: {
35883
+ goalId: decision.goal.id,
35884
+ goalWakeRevision,
35885
+ ...(causalTurn ? { causalTurnId: causalTurn.id } : {}),
35886
+ },
35887
+ personalConnectionDelegations,
34845
35888
  state: "pending",
34846
35889
  })
34847
35890
  .returning();
@@ -35195,6 +36238,10 @@ export async function initializeSessionStartAtomically(
35195
36238
  : {},
35196
36239
  lineage: {},
35197
36240
  ...initiatorColumns(creator),
36241
+ personalConnectionDelegations: parsedPersonalConnectionDelegations(
36242
+ session.initialPersonalConnectionDelegations,
36243
+ `sessions:${session.workspaceId}:${session.id}:initial`,
36244
+ ),
35198
36245
  })
35199
36246
  .returning();
35200
36247
  if (!turn) throw new Error("Failed to create initial session turn");
@@ -35355,6 +36402,7 @@ export async function enqueueSessionTurn(
35355
36402
  initiator: input.initiator,
35356
36403
  context: input.initiatorContext ?? {},
35357
36404
  }),
36405
+ personalConnectionDelegations: input.personalConnectionDelegations ?? [],
35358
36406
  })
35359
36407
  .returning();
35360
36408
  if (!row) {
@@ -35392,7 +36440,14 @@ const MAX_INTERNAL_UPDATE_EVENT_SOURCE_BYTES = 256;
35392
36440
 
35393
36441
  type BoundedSystemUpdate = Pick<
35394
36442
  typeof schema.sessionSystemUpdates.$inferSelect,
35395
- "id" | "kind" | "classification" | "sourceId" | "summary" | "payload" | "lineage"
36443
+ | "id"
36444
+ | "kind"
36445
+ | "classification"
36446
+ | "sourceId"
36447
+ | "summary"
36448
+ | "payload"
36449
+ | "lineage"
36450
+ | "personalConnectionDelegations"
35396
36451
  >;
35397
36452
 
35398
36453
  function boundedInternalUpdateEventText(
@@ -35431,10 +36486,14 @@ function internalUpdateEventMember(update: BoundedSystemUpdate) {
35431
36486
  };
35432
36487
  }
35433
36488
 
35434
- function selectBoundedSystemUpdateBatch<T extends BoundedSystemUpdate>(updates: readonly T[]): T[] {
36489
+ function selectBoundedSystemUpdateBatch<T extends BoundedSystemUpdate>(
36490
+ updates: readonly T[],
36491
+ canCoalesce: (first: T, candidate: T) => boolean = () => true,
36492
+ ): T[] {
35435
36493
  const selected: T[] = [];
35436
36494
  let selectedBytes = 0;
35437
36495
  for (const update of updates) {
36496
+ if (selected[0] && !canCoalesce(selected[0], update)) break;
35438
36497
  const updateBytes = Buffer.byteLength(
35439
36498
  JSON.stringify({
35440
36499
  id: update.id,
@@ -35594,7 +36653,17 @@ export async function claimSessionWorkForAttempt(
35594
36653
  }
35595
36654
  validUpdates.push(update);
35596
36655
  }
35597
- const deliverable = selectBoundedSystemUpdateBatch(validUpdates);
36656
+ const delegationKey = (update: (typeof validUpdates)[number]): string =>
36657
+ stableJson(
36658
+ parsedPersonalConnectionDelegations(
36659
+ update.personalConnectionDelegations,
36660
+ `session_system_updates:${workspaceId}:${sessionId}:${update.id}`,
36661
+ ),
36662
+ );
36663
+ const deliverable = selectBoundedSystemUpdateBatch(
36664
+ validUpdates,
36665
+ (first, candidate) => delegationKey(first) === delegationKey(candidate),
36666
+ );
35598
36667
  if (deliverable.length === 0) {
35599
36668
  const cancellationEvent =
35600
36669
  cancelledUpdateIds.length > 0
@@ -35827,6 +36896,29 @@ export async function claimSessionWorkForAttempt(
35827
36896
  const mcpApprovalPolicies: Record<string, SessionMcpApprovalPolicy> = Object.fromEntries(
35828
36897
  policyRows.map((row) => [row.serverId, row.requireApproval ?? false]),
35829
36898
  );
36899
+ const connectorPolicyRows = await tx
36900
+ .select({
36901
+ id: schema.connectorActionPolicies.id,
36902
+ connectionId: schema.connectorActionPolicies.connectionId,
36903
+ serverId: schema.connectorActionPolicies.serverId,
36904
+ toolName: schema.connectorActionPolicies.toolName,
36905
+ actionName: schema.connectorActionPolicies.actionName,
36906
+ policy: schema.connectorActionPolicies.policy,
36907
+ version: schema.connectorActionPolicies.version,
36908
+ })
36909
+ .from(schema.connectorActionPolicies)
36910
+ .where(eq(schema.connectorActionPolicies.workspaceId, workspaceId))
36911
+ .orderBy(
36912
+ asc(schema.connectorActionPolicies.connectionId),
36913
+ asc(schema.connectorActionPolicies.serverId),
36914
+ asc(schema.connectorActionPolicies.toolName),
36915
+ asc(schema.connectorActionPolicies.actionName),
36916
+ asc(schema.connectorActionPolicies.id),
36917
+ )
36918
+ .limit(2049);
36919
+ if (connectorPolicyRows.length > 2048) {
36920
+ throw new Error("Connector action policy snapshot exceeds the 2048-row bound");
36921
+ }
35830
36922
  return await registerSessionTurnAttemptClaim(tx as unknown as Database, {
35831
36923
  id: input.attemptId,
35832
36924
  accountId: session.accountId,
@@ -35839,6 +36931,7 @@ export async function claimSessionWorkForAttempt(
35839
36931
  temporalActivityId: input.dispatchId,
35840
36932
  verifiedControlRevision: Number(workspaceControl.revision),
35841
36933
  mcpApprovalPolicies,
36934
+ connectorActionPolicies: connectorPolicyRows,
35842
36935
  });
35843
36936
  };
35844
36937
  if (session.activeTurnId !== null) {
@@ -36236,6 +37329,7 @@ export async function claimSessionWorkForAttempt(
36236
37329
  },
36237
37330
  ),
36238
37331
  ...initiatorColumns(compactionInitiator),
37332
+ personalConnectionDelegations: [],
36239
37333
  startedAt: now,
36240
37334
  })
36241
37335
  .returning();
@@ -36383,6 +37477,12 @@ export async function claimSessionWorkForAttempt(
36383
37477
  },
36384
37478
  });
36385
37479
  let internalInitiator: FrozenTurnInitiator;
37480
+ const authorityUpdate = delivered.updates[0];
37481
+ if (!authorityUpdate) throw new Error("Delivered update batch has no authority source");
37482
+ const internalPersonalConnectionDelegations = parsedPersonalConnectionDelegations(
37483
+ authorityUpdate.personalConnectionDelegations,
37484
+ `session_system_updates:${workspaceId}:${sessionId}:${authorityUpdate.id}`,
37485
+ );
36386
37486
  // Agent Steer is the causal command for this inference. Ordinary
36387
37487
  // machine notices may coalesce into the same batch as context, but
36388
37488
  // their timing must not erase the steering subject's authority.
@@ -36544,6 +37644,7 @@ export async function claimSessionWorkForAttempt(
36544
37644
  { id: input.dispatchId, generation: 1, triggerEventId },
36545
37645
  ),
36546
37646
  ...initiatorColumns(internalInitiator),
37647
+ personalConnectionDelegations: internalPersonalConnectionDelegations,
36547
37648
  startedAt: now,
36548
37649
  })
36549
37650
  .returning();
@@ -37877,6 +38978,14 @@ export async function settleSessionIdleWithParentOutbox(
37877
38978
  } as const;
37878
38979
  }
37879
38980
  const dedupeKey = `child-completion:${session.id}:${episodeKey}`;
38981
+ const personalConnectionDelegations = session.parentTurnId
38982
+ ? await personalConnectionDelegationsForTurnInTransaction(
38983
+ tx as unknown as Database,
38984
+ workspaceId,
38985
+ session.parentSessionId,
38986
+ session.parentTurnId,
38987
+ )
38988
+ : [];
37880
38989
  await tx
37881
38990
  .insert(schema.sessionSystemUpdateOutbox)
37882
38991
  .values({
@@ -37897,7 +39006,9 @@ export async function settleSessionIdleWithParentOutbox(
37897
39006
  lineage: {
37898
39007
  childSessionId: session.id,
37899
39008
  parentSessionId: session.parentSessionId,
39009
+ ...(session.parentTurnId ? { parentTurnId: session.parentTurnId } : {}),
37900
39010
  },
39011
+ personalConnectionDelegations,
37901
39012
  })
37902
39013
  .onConflictDoNothing({
37903
39014
  target: [
@@ -39982,6 +41093,16 @@ export async function getSessionQueueSnapshot(
39982
41093
  .where(and(eq(schema.sessions.workspaceId, workspaceId), eq(schema.sessions.id, sessionId)))
39983
41094
  .limit(1);
39984
41095
  if (!session) return null;
41096
+ const activePersonalConnections = session.activeTurnId
41097
+ ? (
41098
+ await personalConnectionDelegationsForTurnInTransaction(
41099
+ scopedDb,
41100
+ workspaceId,
41101
+ sessionId,
41102
+ session.activeTurnId,
41103
+ )
41104
+ ).map(({ serverId, providerDomain }) => ({ serverId, providerDomain }))
41105
+ : [];
39985
41106
  const rows = await scopedDb
39986
41107
  .select()
39987
41108
  .from(schema.sessionTurns)
@@ -40025,6 +41146,7 @@ export async function getSessionQueueSnapshot(
40025
41146
  return {
40026
41147
  version: session.queueVersion,
40027
41148
  effectiveControl: serializeEffectiveSessionControl(effectiveControl),
41149
+ activePersonalConnections,
40028
41150
  stoppingPreviousAttempt:
40029
41151
  latestInterruption !== null &&
40030
41152
  latestInterruption.interruptionState !== "rejected_stale" &&
@@ -40073,11 +41195,19 @@ function queuedSteerReplacementAttemptId(metadata: Record<string, unknown>): str
40073
41195
  async function enqueueFailedChildOutboxForTurnTx(
40074
41196
  tx: Database,
40075
41197
  workspaceId: string,
40076
- session: Pick<typeof schema.sessions.$inferSelect, "id" | "parentSessionId">,
41198
+ session: Pick<typeof schema.sessions.$inferSelect, "id" | "parentSessionId" | "parentTurnId">,
40077
41199
  turn: Pick<typeof schema.sessionTurns.$inferSelect, "id" | "accountId" | "sessionId">,
40078
41200
  ): Promise<void> {
40079
41201
  if (!session.parentSessionId) return;
40080
41202
  const dedupeKey = `child-completion:${turn.sessionId}:turn:${turn.id}`;
41203
+ const personalConnectionDelegations = session.parentTurnId
41204
+ ? await personalConnectionDelegationsForTurnInTransaction(
41205
+ tx,
41206
+ workspaceId,
41207
+ session.parentSessionId,
41208
+ session.parentTurnId,
41209
+ )
41210
+ : [];
40081
41211
  await tx
40082
41212
  .insert(schema.sessionSystemUpdateOutbox)
40083
41213
  .values({
@@ -40099,8 +41229,10 @@ async function enqueueFailedChildOutboxForTurnTx(
40099
41229
  lineage: {
40100
41230
  childSessionId: turn.sessionId,
40101
41231
  parentSessionId: session.parentSessionId,
41232
+ ...(session.parentTurnId ? { parentTurnId: session.parentTurnId } : {}),
40102
41233
  turnId: turn.id,
40103
41234
  },
41235
+ personalConnectionDelegations,
40104
41236
  })
40105
41237
  .onConflictDoNothing({
40106
41238
  target: [
@@ -40137,6 +41269,7 @@ export type SessionSystemUpdateOutboxDelivery = {
40137
41269
  summary: string;
40138
41270
  payload: ChildTerminalResultPayload;
40139
41271
  lineage: Record<string, unknown>;
41272
+ personalConnectionDelegations: McpPersonalConnectionDelegation[];
40140
41273
  };
40141
41274
 
40142
41275
  function mapSystemUpdateOutboxRow(row: {
@@ -40152,6 +41285,7 @@ function mapSystemUpdateOutboxRow(row: {
40152
41285
  summary: string;
40153
41286
  payload: Record<string, unknown>;
40154
41287
  lineage: Record<string, unknown>;
41288
+ personal_connection_delegations: unknown;
40155
41289
  }): SessionSystemUpdateOutboxDelivery {
40156
41290
  if (row.kind !== "child_terminal_result") {
40157
41291
  throw new Error(`System-update outbox contains retired kind ${row.kind}`);
@@ -40170,6 +41304,10 @@ function mapSystemUpdateOutboxRow(row: {
40170
41304
  summary: row.summary,
40171
41305
  payload: parseChildTerminalResultPayload(row.payload),
40172
41306
  lineage: row.lineage,
41307
+ personalConnectionDelegations: parsedPersonalConnectionDelegations(
41308
+ row.personal_connection_delegations,
41309
+ `session_system_update_outbox:${row.workspace_id}:${row.id}`,
41310
+ ),
40173
41311
  };
40174
41312
  }
40175
41313
 
@@ -40214,6 +41352,10 @@ export async function getSessionSystemUpdateOutboxByDedupeKey(
40214
41352
  summary: row.summary,
40215
41353
  payload: parseChildTerminalResultPayload(row.payload),
40216
41354
  lineage: row.lineage,
41355
+ personalConnectionDelegations: parsedPersonalConnectionDelegations(
41356
+ row.personalConnectionDelegations,
41357
+ `session_system_update_outbox:${row.workspaceId}:${row.id}`,
41358
+ ),
40217
41359
  };
40218
41360
  },
40219
41361
  );
@@ -40236,6 +41378,7 @@ export async function claimPendingSessionSystemUpdateOutbox(
40236
41378
  summary: string;
40237
41379
  payload: Record<string, unknown>;
40238
41380
  lineage: Record<string, unknown>;
41381
+ personal_connection_delegations: unknown;
40239
41382
  }>(db, sql`select * from opengeni_private.claim_session_system_update_outbox(${limit})`);
40240
41383
  return rows.map(mapSystemUpdateOutboxRow);
40241
41384
  }
@@ -40586,6 +41729,7 @@ export async function getOrCreateSessionSystemUpdateOutbox(
40586
41729
  summary: input.summary,
40587
41730
  payload: input.payload,
40588
41731
  lineage: input.lineage,
41732
+ personalConnectionDelegations: input.personalConnectionDelegations,
40589
41733
  })
40590
41734
  .onConflictDoUpdate({
40591
41735
  target: [
@@ -40618,6 +41762,10 @@ export async function getOrCreateSessionSystemUpdateOutbox(
40618
41762
  summary: row.summary,
40619
41763
  payload: parseChildTerminalResultPayload(row.payload),
40620
41764
  lineage: row.lineage,
41765
+ personalConnectionDelegations: parsedPersonalConnectionDelegations(
41766
+ row.personalConnectionDelegations,
41767
+ `session_system_update_outbox:${row.workspaceId}:${row.id}`,
41768
+ ),
40621
41769
  };
40622
41770
  });
40623
41771
  }
@@ -40699,6 +41847,7 @@ export type AddSessionSystemUpdateInput = {
40699
41847
  dedupeKey: string;
40700
41848
  summary: string;
40701
41849
  lineage?: Record<string, unknown>;
41850
+ personalConnectionDelegations?: McpPersonalConnectionDelegation[];
40702
41851
  } & SessionSystemUpdateInputVariant;
40703
41852
 
40704
41853
  export type AddSessionSystemUpdateResult =
@@ -40775,6 +41924,7 @@ export async function addSessionSystemUpdateWithSourceMutation(
40775
41924
  summary: input.summary,
40776
41925
  payload: input.payload,
40777
41926
  lineage: input.lineage ?? {},
41927
+ personalConnectionDelegations: input.personalConnectionDelegations ?? [],
40778
41928
  state: "pending",
40779
41929
  })
40780
41930
  .onConflictDoNothing({
@@ -41067,6 +42217,7 @@ export async function acceptSessionApprovalDecision(
41067
42217
  accountId: string;
41068
42218
  workspaceId: string;
41069
42219
  sessionId: string;
42220
+ subjectId: string;
41070
42221
  payload: Record<string, unknown>;
41071
42222
  clientEventId?: string | null;
41072
42223
  },
@@ -41205,6 +42356,41 @@ export async function acceptSessionApprovalDecision(
41205
42356
  })
41206
42357
  .returning();
41207
42358
  if (!event) throw new Error("Failed to append approval decision");
42359
+ const approvalDecision = input.payload.decision;
42360
+ if (approvalDecision === "approve" || approvalDecision === "reject") {
42361
+ const [connectorRequest] = await tx
42362
+ .update(schema.connectorActionRequests)
42363
+ .set({
42364
+ status: approvalDecision === "approve" ? "approved" : "rejected",
42365
+ decision: approvalDecision,
42366
+ decisionBySubjectId: boundedConnectorActionText(
42367
+ input.subjectId,
42368
+ "approval actor",
42369
+ 1024,
42370
+ ),
42371
+ decisionEventId: event.id,
42372
+ decidedAt: event.occurredAt,
42373
+ updatedAt: new Date(),
42374
+ })
42375
+ .where(
42376
+ and(
42377
+ eq(schema.connectorActionRequests.workspaceId, input.workspaceId),
42378
+ eq(schema.connectorActionRequests.sessionId, input.sessionId),
42379
+ eq(schema.connectorActionRequests.turnId, turn.id),
42380
+ eq(schema.connectorActionRequests.approvalId, approvalId),
42381
+ eq(schema.connectorActionRequests.status, "pending"),
42382
+ ),
42383
+ )
42384
+ .returning();
42385
+ if (connectorRequest) {
42386
+ await insertConnectorActionAudit(tx as unknown as Database, {
42387
+ row: connectorRequest,
42388
+ action: "connector.action.approval_decided",
42389
+ subjectId: input.subjectId,
42390
+ extra: { decision: approvalDecision },
42391
+ });
42392
+ }
42393
+ }
41208
42394
  await tx
41209
42395
  .update(schema.sessions)
41210
42396
  .set({
@@ -41933,6 +43119,10 @@ function mapSessionTurn(row: typeof schema.sessionTurns.$inferSelect): SessionTu
41933
43119
  row.initiatorContext ?? {},
41934
43120
  ),
41935
43121
  initiatorContext: row.initiatorContext ?? {},
43122
+ personalConnections: parsedPersonalConnectionDelegations(
43123
+ row.personalConnectionDelegations,
43124
+ `session_turns:${row.workspaceId}:${row.sessionId}:${row.id}`,
43125
+ ).map(({ serverId, providerDomain }) => ({ serverId, providerDomain })),
41936
43126
  cancelledBy: row.cancelledBy,
41937
43127
  cancelReason: row.cancelReason,
41938
43128
  startedAt: row.startedAt ? row.startedAt.toISOString() : null,
@@ -41948,6 +43138,10 @@ function mapSessionTurnForExecution(
41948
43138
  return {
41949
43139
  ...mapSessionTurn(row),
41950
43140
  turnInstructions: row.turnInstructions ?? null,
43141
+ personalConnectionDelegations: parsedPersonalConnectionDelegations(
43142
+ row.personalConnectionDelegations,
43143
+ `session_turns:${row.workspaceId}:${row.sessionId}:${row.id}`,
43144
+ ),
41951
43145
  };
41952
43146
  }
41953
43147
 
@@ -42011,6 +43205,16 @@ function mapScheduledTask(row: typeof schema.scheduledTasks.$inferSelect): Sched
42011
43205
  runMode: row.runMode as ScheduledTaskRunMode,
42012
43206
  overlapPolicy: row.overlapPolicy as ScheduledTaskOverlapPolicy,
42013
43207
  agentConfig: row.agentConfig as ScheduledTaskAgentConfig,
43208
+ createdBy: initiatorFromStorage(
43209
+ row.createdByKind,
43210
+ row.createdBySubjectId,
43211
+ row.createdByContext as TurnInitiatorContext,
43212
+ ),
43213
+ createdByContext: row.createdByContext as TurnInitiatorContext,
43214
+ personalConnections: parsedPersonalConnectionDelegations(
43215
+ row.personalConnectionDelegations,
43216
+ `scheduled_tasks:${row.workspaceId}:${row.id}`,
43217
+ ).map(({ serverId, providerDomain }) => ({ serverId, providerDomain })),
42014
43218
  reusableSessionId: row.reusableSessionId,
42015
43219
  variableSetId: row.variableSetId,
42016
43220
  environmentId: row.variableSetId,