@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/dist/{chunk-OTJHD33E.js → chunk-BNGEN5QZ.js} +5 -1
- package/dist/chunk-BNGEN5QZ.js.map +1 -0
- package/dist/{chunk-NK2A36KP.js → chunk-CYGFLLMN.js} +150 -1
- package/dist/chunk-CYGFLLMN.js.map +1 -0
- package/dist/index.d.ts +87 -0
- package/dist/index.js +599 -6
- package/dist/index.js.map +1 -1
- package/dist/provision-roles.js +1 -1
- package/dist/runtime-posture.d.ts +2 -2
- package/dist/schema.d.ts +820 -0
- package/dist/schema.js +5 -1
- package/dist/session-control.d.ts +1 -0
- package/drizzle/0155_connector_action_policies.sql +218 -0
- package/package.json +3 -3
- package/src/index.ts +926 -1
- package/src/runtime-posture.ts +4 -0
- package/src/schema.ts +181 -0
- package/src/session-control.ts +3 -0
- package/dist/chunk-NK2A36KP.js.map +0 -1
- package/dist/chunk-OTJHD33E.js.map +0 -1
package/src/index.ts
CHANGED
|
@@ -146,7 +146,12 @@ import {
|
|
|
146
146
|
SubmitHumanInputResponseRequest,
|
|
147
147
|
TurnExecutionPolicyV1,
|
|
148
148
|
} from "@opengeni/contracts";
|
|
149
|
-
import {
|
|
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({
|