@opengeni/db 0.13.0 → 0.13.4
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-N4WTE6SB.js → chunk-IQOXTFKA.js} +2 -1
- package/dist/chunk-IQOXTFKA.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +397 -173
- package/dist/index.js.map +1 -1
- package/dist/provision-roles.d.ts +44 -6
- package/dist/{schema-Bh1Hr7xY.d.ts → schema-SbaSoRdr.d.ts} +34 -15
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +1 -1
- package/drizzle/0132_connection_subject_isolation.sql +100 -0
- package/drizzle/0133_session_skills.sql +3 -0
- package/package.json +3 -3
- package/src/connection-token-resolver.ts +17 -7
- package/src/index.ts +554 -202
- package/src/schema.ts +1 -0
- package/dist/chunk-N4WTE6SB.js.map +0 -1
package/src/index.ts
CHANGED
|
@@ -59,6 +59,7 @@ import type {
|
|
|
59
59
|
SessionHumanInputRequest,
|
|
60
60
|
LineageNode,
|
|
61
61
|
SessionMcpApprovalPolicy,
|
|
62
|
+
SessionSkill,
|
|
62
63
|
SessionMcpServerMetadata,
|
|
63
64
|
SessionStatus,
|
|
64
65
|
SessionToolPolicy,
|
|
@@ -1338,6 +1339,24 @@ export async function bootstrapWorkspace(
|
|
|
1338
1339
|
})
|
|
1339
1340
|
.where(eq(schema.workspaceMemberships.id, membership.id));
|
|
1340
1341
|
}
|
|
1342
|
+
// Access refreshes must retain workspaces created or granted after the
|
|
1343
|
+
// default workspace. Restore account-scoped RLS before listing them.
|
|
1344
|
+
await setRlsContext(tx as unknown as Database, {
|
|
1345
|
+
accountId: workspace.accountId,
|
|
1346
|
+
workspaceId: null,
|
|
1347
|
+
});
|
|
1348
|
+
const memberships = await tx
|
|
1349
|
+
.select({
|
|
1350
|
+
membership: schema.workspaceMemberships,
|
|
1351
|
+
workspace: schema.workspaces,
|
|
1352
|
+
})
|
|
1353
|
+
.from(schema.workspaceMemberships)
|
|
1354
|
+
.innerJoin(
|
|
1355
|
+
schema.workspaces,
|
|
1356
|
+
eq(schema.workspaceMemberships.workspaceId, schema.workspaces.id),
|
|
1357
|
+
)
|
|
1358
|
+
.where(eq(schema.workspaceMemberships.subjectId, input.subjectId))
|
|
1359
|
+
.orderBy(desc(schema.workspaces.createdAt));
|
|
1341
1360
|
return {
|
|
1342
1361
|
mode: input.accountExternalSource === "opengeni:local" ? "local" : "configured",
|
|
1343
1362
|
subjectId: input.subjectId,
|
|
@@ -1351,15 +1370,13 @@ export async function bootstrapWorkspace(
|
|
|
1351
1370
|
permissions: input.accountPermissions ?? allAccountPermissions,
|
|
1352
1371
|
},
|
|
1353
1372
|
],
|
|
1354
|
-
workspaceGrants:
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
},
|
|
1362
|
-
],
|
|
1373
|
+
workspaceGrants: memberships.map((row) => ({
|
|
1374
|
+
workspaceId: row.workspace.id,
|
|
1375
|
+
accountId: row.workspace.accountId,
|
|
1376
|
+
subjectId: input.subjectId,
|
|
1377
|
+
...(input.subjectLabel ? { subjectLabel: input.subjectLabel } : {}),
|
|
1378
|
+
permissions: row.membership.permissions as Permission[],
|
|
1379
|
+
})),
|
|
1363
1380
|
defaultAccountId: account.id,
|
|
1364
1381
|
defaultWorkspaceId: workspace.id,
|
|
1365
1382
|
};
|
|
@@ -3328,6 +3345,7 @@ export type RegistryCapabilityCatalogItemInput = {
|
|
|
3328
3345
|
importBatchId: string;
|
|
3329
3346
|
scopesHint?: string[];
|
|
3330
3347
|
homepageUrl?: string | null;
|
|
3348
|
+
installUrl?: string | null;
|
|
3331
3349
|
tags?: string[];
|
|
3332
3350
|
metadata?: Record<string, unknown>;
|
|
3333
3351
|
};
|
|
@@ -4196,7 +4214,7 @@ export async function upsertRegistryCapabilityCatalogItem(
|
|
|
4196
4214
|
tags: input.tags ?? ["mcp", "integration", input.tier],
|
|
4197
4215
|
homepageUrl: input.homepageUrl ?? `https://${input.providerDomain}`,
|
|
4198
4216
|
endpointUrl: input.mcpUrl,
|
|
4199
|
-
installUrl: input.homepageUrl ?? `https://${input.providerDomain}`,
|
|
4217
|
+
installUrl: input.installUrl ?? input.homepageUrl ?? `https://${input.providerDomain}`,
|
|
4200
4218
|
authModel: input.authKind === "none" ? null : "credential_ref",
|
|
4201
4219
|
providerDomain: input.providerDomain,
|
|
4202
4220
|
surfaceType: "mcp",
|
|
@@ -4771,6 +4789,52 @@ function connectionSubjectVisibility(subjectId?: string | null): SQL {
|
|
|
4771
4789
|
: isNull(schema.connections.subjectId);
|
|
4772
4790
|
}
|
|
4773
4791
|
|
|
4792
|
+
function connectionExactSubject(subjectId?: string | null): SQL {
|
|
4793
|
+
return subjectId
|
|
4794
|
+
? eq(schema.connections.subjectId, subjectId)
|
|
4795
|
+
: isNull(schema.connections.subjectId);
|
|
4796
|
+
}
|
|
4797
|
+
|
|
4798
|
+
async function withConnectionSubjectRls<T>(
|
|
4799
|
+
db: Database,
|
|
4800
|
+
workspaceId: string,
|
|
4801
|
+
subjectId: string | null | undefined,
|
|
4802
|
+
fn: (db: Database) => Promise<T>,
|
|
4803
|
+
): Promise<T> {
|
|
4804
|
+
return subjectId
|
|
4805
|
+
? await withWorkspaceSubjectRls(db, workspaceId, subjectId, fn)
|
|
4806
|
+
: await withWorkspaceRls(db, workspaceId, fn);
|
|
4807
|
+
}
|
|
4808
|
+
|
|
4809
|
+
async function createConnectionInScope(
|
|
4810
|
+
db: Database,
|
|
4811
|
+
input: CreateConnectionInput,
|
|
4812
|
+
): Promise<ConnectionMetadataWithVerification> {
|
|
4813
|
+
const [row] = await db
|
|
4814
|
+
.insert(schema.connections)
|
|
4815
|
+
.values({
|
|
4816
|
+
accountId: input.accountId,
|
|
4817
|
+
workspaceId: input.workspaceId,
|
|
4818
|
+
subjectId: input.subjectId ?? null,
|
|
4819
|
+
providerDomain: input.providerDomain,
|
|
4820
|
+
kind: input.kind,
|
|
4821
|
+
status: input.status ?? "active",
|
|
4822
|
+
credentialEncrypted: input.credentialEncrypted,
|
|
4823
|
+
grantedScopes: input.grantedScopes ?? [],
|
|
4824
|
+
expiresAt: input.expiresAt ?? null,
|
|
4825
|
+
verifiedInstallAt: input.verifiedInstallAt ?? null,
|
|
4826
|
+
verifiedInstallVersion: input.verifiedInstallVersion ?? null,
|
|
4827
|
+
metadata: input.metadata ?? {},
|
|
4828
|
+
createdBySubjectId: input.createdBySubjectId ?? null,
|
|
4829
|
+
updatedBySubjectId: input.updatedBySubjectId ?? input.createdBySubjectId ?? null,
|
|
4830
|
+
})
|
|
4831
|
+
.returning(connectionMetadataColumns);
|
|
4832
|
+
if (!row) {
|
|
4833
|
+
throw new Error("Failed to create connection");
|
|
4834
|
+
}
|
|
4835
|
+
return mapConnectionMetadata(row);
|
|
4836
|
+
}
|
|
4837
|
+
|
|
4774
4838
|
export async function createConnection(
|
|
4775
4839
|
db: Database,
|
|
4776
4840
|
input: CreateConnectionInput,
|
|
@@ -4779,29 +4843,10 @@ export async function createConnection(
|
|
|
4779
4843
|
db,
|
|
4780
4844
|
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
4781
4845
|
async (scopedDb) => {
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
.values({
|
|
4785
|
-
accountId: input.accountId,
|
|
4786
|
-
workspaceId: input.workspaceId,
|
|
4787
|
-
subjectId: input.subjectId ?? null,
|
|
4788
|
-
providerDomain: input.providerDomain,
|
|
4789
|
-
kind: input.kind,
|
|
4790
|
-
status: input.status ?? "active",
|
|
4791
|
-
credentialEncrypted: input.credentialEncrypted,
|
|
4792
|
-
grantedScopes: input.grantedScopes ?? [],
|
|
4793
|
-
expiresAt: input.expiresAt ?? null,
|
|
4794
|
-
verifiedInstallAt: input.verifiedInstallAt ?? null,
|
|
4795
|
-
verifiedInstallVersion: input.verifiedInstallVersion ?? null,
|
|
4796
|
-
metadata: input.metadata ?? {},
|
|
4797
|
-
createdBySubjectId: input.createdBySubjectId ?? null,
|
|
4798
|
-
updatedBySubjectId: input.updatedBySubjectId ?? input.createdBySubjectId ?? null,
|
|
4799
|
-
})
|
|
4800
|
-
.returning(connectionMetadataColumns);
|
|
4801
|
-
if (!row) {
|
|
4802
|
-
throw new Error("Failed to create connection");
|
|
4846
|
+
if (input.subjectId) {
|
|
4847
|
+
await setSubjectRlsContext(scopedDb, input.subjectId);
|
|
4803
4848
|
}
|
|
4804
|
-
return
|
|
4849
|
+
return await createConnectionInScope(scopedDb, input);
|
|
4805
4850
|
},
|
|
4806
4851
|
);
|
|
4807
4852
|
}
|
|
@@ -4811,7 +4856,7 @@ export async function listConnectionsMetadata(
|
|
|
4811
4856
|
workspaceId: string,
|
|
4812
4857
|
subjectId?: string | null,
|
|
4813
4858
|
): Promise<ConnectionMetadataWithVerification[]> {
|
|
4814
|
-
return await
|
|
4859
|
+
return await withConnectionSubjectRls(db, workspaceId, subjectId, async (scopedDb) => {
|
|
4815
4860
|
const rows = await scopedDb
|
|
4816
4861
|
.select(connectionMetadataColumns)
|
|
4817
4862
|
.from(schema.connections)
|
|
@@ -4832,7 +4877,7 @@ export async function getConnectionMetadata(
|
|
|
4832
4877
|
connectionId: string,
|
|
4833
4878
|
subjectId?: string | null,
|
|
4834
4879
|
): Promise<ConnectionMetadataWithVerification | null> {
|
|
4835
|
-
return await
|
|
4880
|
+
return await withConnectionSubjectRls(db, workspaceId, subjectId, async (scopedDb) => {
|
|
4836
4881
|
const [row] = await scopedDb
|
|
4837
4882
|
.select(connectionMetadataColumns)
|
|
4838
4883
|
.from(schema.connections)
|
|
@@ -4848,53 +4893,102 @@ export async function getConnectionMetadata(
|
|
|
4848
4893
|
});
|
|
4849
4894
|
}
|
|
4850
4895
|
|
|
4896
|
+
async function updateConnectionInScope(
|
|
4897
|
+
db: Database,
|
|
4898
|
+
input: UpdateConnectionInput,
|
|
4899
|
+
): Promise<ConnectionMetadataWithVerification | null> {
|
|
4900
|
+
const set = {
|
|
4901
|
+
updatedAt: new Date(),
|
|
4902
|
+
...(input.providerDomain !== undefined ? { providerDomain: input.providerDomain } : {}),
|
|
4903
|
+
...(input.subjectId !== undefined ? { subjectId: input.subjectId } : {}),
|
|
4904
|
+
...(input.kind !== undefined ? { kind: input.kind } : {}),
|
|
4905
|
+
...(input.status !== undefined ? { status: input.status } : {}),
|
|
4906
|
+
...(input.credentialEncrypted !== undefined
|
|
4907
|
+
? {
|
|
4908
|
+
credentialEncrypted: input.credentialEncrypted,
|
|
4909
|
+
version: sql`${schema.connections.version} + 1`,
|
|
4910
|
+
lastError: null,
|
|
4911
|
+
}
|
|
4912
|
+
: {}),
|
|
4913
|
+
...(input.grantedScopes !== undefined ? { grantedScopes: input.grantedScopes } : {}),
|
|
4914
|
+
...(input.expiresAt !== undefined ? { expiresAt: input.expiresAt } : {}),
|
|
4915
|
+
...(input.verifiedInstallAt !== undefined
|
|
4916
|
+
? { verifiedInstallAt: input.verifiedInstallAt }
|
|
4917
|
+
: {}),
|
|
4918
|
+
...(input.verifiedInstallVersion !== undefined
|
|
4919
|
+
? { verifiedInstallVersion: input.verifiedInstallVersion }
|
|
4920
|
+
: {}),
|
|
4921
|
+
...(input.metadata !== undefined ? { metadata: input.metadata } : {}),
|
|
4922
|
+
...(input.updatedBySubjectId !== undefined
|
|
4923
|
+
? { updatedBySubjectId: input.updatedBySubjectId }
|
|
4924
|
+
: {}),
|
|
4925
|
+
};
|
|
4926
|
+
const [row] = await db
|
|
4927
|
+
.update(schema.connections)
|
|
4928
|
+
.set(set)
|
|
4929
|
+
.where(
|
|
4930
|
+
and(
|
|
4931
|
+
eq(schema.connections.workspaceId, input.workspaceId),
|
|
4932
|
+
eq(schema.connections.id, input.connectionId),
|
|
4933
|
+
connectionSubjectVisibility(input.visibleToSubjectId),
|
|
4934
|
+
...(input.expectedVersion !== undefined
|
|
4935
|
+
? [eq(schema.connections.version, input.expectedVersion)]
|
|
4936
|
+
: []),
|
|
4937
|
+
),
|
|
4938
|
+
)
|
|
4939
|
+
.returning(connectionMetadataColumns);
|
|
4940
|
+
return row ? mapConnectionMetadata(row) : null;
|
|
4941
|
+
}
|
|
4942
|
+
|
|
4851
4943
|
export async function updateConnection(
|
|
4852
4944
|
db: Database,
|
|
4853
4945
|
input: UpdateConnectionInput,
|
|
4854
4946
|
): Promise<ConnectionMetadataWithVerification | null> {
|
|
4855
|
-
return await
|
|
4856
|
-
|
|
4947
|
+
return await withConnectionSubjectRls(
|
|
4948
|
+
db,
|
|
4949
|
+
input.workspaceId,
|
|
4950
|
+
input.visibleToSubjectId,
|
|
4951
|
+
async (scopedDb) => await updateConnectionInScope(scopedDb, input),
|
|
4952
|
+
);
|
|
4953
|
+
}
|
|
4954
|
+
|
|
4955
|
+
async function revokeConnectionInScope(
|
|
4956
|
+
db: Database,
|
|
4957
|
+
workspaceId: string,
|
|
4958
|
+
connectionId: string,
|
|
4959
|
+
updatedBySubjectId?: string | null,
|
|
4960
|
+
expectedVersion?: number,
|
|
4961
|
+
): Promise<ConnectionMetadataWithVerification | null> {
|
|
4962
|
+
const [row] = await db
|
|
4963
|
+
.update(schema.connections)
|
|
4964
|
+
.set({
|
|
4965
|
+
status: "revoked",
|
|
4966
|
+
// The version bump invalidates any in-flight refresh's (id, version) CAS,
|
|
4967
|
+
// so a racing refresh cannot commit and flip the row back to active.
|
|
4968
|
+
version: sql`${schema.connections.version} + 1`,
|
|
4969
|
+
// Status-only revocation does not replace the verified credential or bot
|
|
4970
|
+
// identity. Carry the marker to the same new CAS version so the dedicated
|
|
4971
|
+
// reinstall path can still recognize (but not use) the inactive row.
|
|
4972
|
+
verifiedInstallVersion: sql`case
|
|
4973
|
+
when ${schema.connections.verifiedInstallAt} is null then null
|
|
4974
|
+
else ${schema.connections.version} + 1
|
|
4975
|
+
end`,
|
|
4976
|
+
updatedBySubjectId: updatedBySubjectId ?? null,
|
|
4857
4977
|
updatedAt: new Date(),
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
? { verifiedInstallAt: input.verifiedInstallAt }
|
|
4873
|
-
: {}),
|
|
4874
|
-
...(input.verifiedInstallVersion !== undefined
|
|
4875
|
-
? { verifiedInstallVersion: input.verifiedInstallVersion }
|
|
4876
|
-
: {}),
|
|
4877
|
-
...(input.metadata !== undefined ? { metadata: input.metadata } : {}),
|
|
4878
|
-
...(input.updatedBySubjectId !== undefined
|
|
4879
|
-
? { updatedBySubjectId: input.updatedBySubjectId }
|
|
4880
|
-
: {}),
|
|
4881
|
-
};
|
|
4882
|
-
const [row] = await scopedDb
|
|
4883
|
-
.update(schema.connections)
|
|
4884
|
-
.set(set)
|
|
4885
|
-
.where(
|
|
4886
|
-
and(
|
|
4887
|
-
eq(schema.connections.workspaceId, input.workspaceId),
|
|
4888
|
-
eq(schema.connections.id, input.connectionId),
|
|
4889
|
-
connectionSubjectVisibility(input.visibleToSubjectId),
|
|
4890
|
-
...(input.expectedVersion !== undefined
|
|
4891
|
-
? [eq(schema.connections.version, input.expectedVersion)]
|
|
4892
|
-
: []),
|
|
4893
|
-
),
|
|
4894
|
-
)
|
|
4895
|
-
.returning(connectionMetadataColumns);
|
|
4896
|
-
return row ? mapConnectionMetadata(row) : null;
|
|
4897
|
-
});
|
|
4978
|
+
})
|
|
4979
|
+
.where(
|
|
4980
|
+
and(
|
|
4981
|
+
eq(schema.connections.workspaceId, workspaceId),
|
|
4982
|
+
eq(schema.connections.id, connectionId),
|
|
4983
|
+
// Same visibility rule as get/update: shared rows plus the caller's own
|
|
4984
|
+
// subject rows. Cross-subject revocation (admin janitorial) arrives with
|
|
4985
|
+
// the subject-connections UX in I5, deliberately not before.
|
|
4986
|
+
connectionSubjectVisibility(updatedBySubjectId),
|
|
4987
|
+
...(expectedVersion !== undefined ? [eq(schema.connections.version, expectedVersion)] : []),
|
|
4988
|
+
),
|
|
4989
|
+
)
|
|
4990
|
+
.returning(connectionMetadataColumns);
|
|
4991
|
+
return row ? mapConnectionMetadata(row) : null;
|
|
4898
4992
|
}
|
|
4899
4993
|
|
|
4900
4994
|
export async function revokeConnection(
|
|
@@ -4903,39 +4997,244 @@ export async function revokeConnection(
|
|
|
4903
4997
|
connectionId: string,
|
|
4904
4998
|
updatedBySubjectId?: string | null,
|
|
4905
4999
|
): Promise<ConnectionMetadataWithVerification | null> {
|
|
4906
|
-
return await
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
5000
|
+
return await withConnectionSubjectRls(
|
|
5001
|
+
db,
|
|
5002
|
+
workspaceId,
|
|
5003
|
+
updatedBySubjectId,
|
|
5004
|
+
async (scopedDb) =>
|
|
5005
|
+
await revokeConnectionInScope(scopedDb, workspaceId, connectionId, updatedBySubjectId),
|
|
5006
|
+
);
|
|
5007
|
+
}
|
|
5008
|
+
|
|
5009
|
+
export class SlackBotLifecycleSuccessAuditError extends Error {
|
|
5010
|
+
constructor() {
|
|
5011
|
+
super("OpenGeni Slack bot lifecycle success audit failed");
|
|
5012
|
+
this.name = "SlackBotLifecycleSuccessAuditError";
|
|
5013
|
+
}
|
|
5014
|
+
}
|
|
5015
|
+
|
|
5016
|
+
type SlackBotLifecycleSuccessAuditInput = {
|
|
5017
|
+
accountId: string;
|
|
5018
|
+
workspaceId: string;
|
|
5019
|
+
subjectId: string;
|
|
5020
|
+
credentialRole: string;
|
|
5021
|
+
credentialLabel: string;
|
|
5022
|
+
slackTeamId: string;
|
|
5023
|
+
};
|
|
5024
|
+
|
|
5025
|
+
async function insertSlackBotLifecycleSuccessAuditInScope(
|
|
5026
|
+
db: Database,
|
|
5027
|
+
input: SlackBotLifecycleSuccessAuditInput & {
|
|
5028
|
+
action: "slack_bot.connected" | "slack_bot.reinstalled" | "slack_bot.disconnected";
|
|
5029
|
+
connectionId: string;
|
|
5030
|
+
},
|
|
5031
|
+
): Promise<void> {
|
|
5032
|
+
try {
|
|
5033
|
+
await db.insert(schema.auditEvents).values({
|
|
5034
|
+
accountId: input.accountId,
|
|
5035
|
+
workspaceId: input.workspaceId,
|
|
5036
|
+
subjectId: input.subjectId,
|
|
5037
|
+
action: input.action,
|
|
5038
|
+
targetType: "connection",
|
|
5039
|
+
targetId: input.connectionId,
|
|
5040
|
+
metadata: {
|
|
5041
|
+
credentialRole: input.credentialRole,
|
|
5042
|
+
credentialLabel: input.credentialLabel,
|
|
5043
|
+
connectionId: input.connectionId,
|
|
5044
|
+
slackTeamId: input.slackTeamId,
|
|
5045
|
+
outcome: "succeeded",
|
|
5046
|
+
},
|
|
5047
|
+
});
|
|
5048
|
+
} catch {
|
|
5049
|
+
// Do not leak a provider/database payload through the callback. Throwing from
|
|
5050
|
+
// the RLS transaction is what rolls the paired connection mutation back.
|
|
5051
|
+
throw new SlackBotLifecycleSuccessAuditError();
|
|
5052
|
+
}
|
|
5053
|
+
}
|
|
5054
|
+
|
|
5055
|
+
async function assertWorkspaceAccountPairInScope(
|
|
5056
|
+
db: Database,
|
|
5057
|
+
accountId: string,
|
|
5058
|
+
workspaceId: string,
|
|
5059
|
+
): Promise<void> {
|
|
5060
|
+
const [workspace] = await db
|
|
5061
|
+
.select({ id: schema.workspaces.id })
|
|
5062
|
+
.from(schema.workspaces)
|
|
5063
|
+
.where(and(eq(schema.workspaces.id, workspaceId), eq(schema.workspaces.accountId, accountId)))
|
|
5064
|
+
.limit(1);
|
|
5065
|
+
if (!workspace) {
|
|
5066
|
+
throw new Error("Workspace does not belong to the expected account");
|
|
5067
|
+
}
|
|
5068
|
+
}
|
|
5069
|
+
|
|
5070
|
+
async function withSlackBotLifecycleRls<T>(
|
|
5071
|
+
db: Database,
|
|
5072
|
+
input: Pick<SlackBotLifecycleSuccessAuditInput, "accountId" | "workspaceId" | "subjectId">,
|
|
5073
|
+
fn: (db: Database) => Promise<T>,
|
|
5074
|
+
): Promise<T> {
|
|
5075
|
+
return await withRlsContext(
|
|
5076
|
+
db,
|
|
5077
|
+
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
5078
|
+
async (scopedDb) => {
|
|
5079
|
+
await setSubjectRlsContext(scopedDb, input.subjectId);
|
|
5080
|
+
await assertWorkspaceAccountPairInScope(scopedDb, input.accountId, input.workspaceId);
|
|
5081
|
+
return await fn(scopedDb);
|
|
5082
|
+
},
|
|
5083
|
+
);
|
|
5084
|
+
}
|
|
5085
|
+
|
|
5086
|
+
export async function createConnectionWithSlackBotSuccessAudit(
|
|
5087
|
+
db: Database,
|
|
5088
|
+
input: SlackBotLifecycleSuccessAuditInput & { connection: CreateConnectionInput },
|
|
5089
|
+
): Promise<ConnectionMetadataWithVerification> {
|
|
5090
|
+
if (
|
|
5091
|
+
input.connection.accountId !== input.accountId ||
|
|
5092
|
+
input.connection.workspaceId !== input.workspaceId
|
|
5093
|
+
) {
|
|
5094
|
+
throw new Error("Slack bot connection and lifecycle audit tenant must match");
|
|
5095
|
+
}
|
|
5096
|
+
return await withSlackBotLifecycleRls(db, input, async (scopedDb) => {
|
|
5097
|
+
const connection = await createConnectionInScope(scopedDb, input.connection);
|
|
5098
|
+
await insertSlackBotLifecycleSuccessAuditInScope(scopedDb, {
|
|
5099
|
+
...input,
|
|
5100
|
+
action: "slack_bot.connected",
|
|
5101
|
+
connectionId: connection.id,
|
|
5102
|
+
});
|
|
5103
|
+
return connection;
|
|
5104
|
+
});
|
|
5105
|
+
}
|
|
5106
|
+
|
|
5107
|
+
export async function updateConnectionWithSlackBotSuccessAudit(
|
|
5108
|
+
db: Database,
|
|
5109
|
+
input: SlackBotLifecycleSuccessAuditInput & { connection: UpdateConnectionInput },
|
|
5110
|
+
): Promise<ConnectionMetadataWithVerification | null> {
|
|
5111
|
+
if (input.connection.workspaceId !== input.workspaceId) {
|
|
5112
|
+
throw new Error("Slack bot connection and lifecycle audit workspace must match");
|
|
5113
|
+
}
|
|
5114
|
+
return await withSlackBotLifecycleRls(db, input, async (scopedDb) => {
|
|
5115
|
+
const connection = await updateConnectionInScope(scopedDb, input.connection);
|
|
5116
|
+
if (!connection) return null;
|
|
5117
|
+
if (connection.accountId !== input.accountId) {
|
|
5118
|
+
throw new Error("Slack bot connection and lifecycle audit account must match");
|
|
5119
|
+
}
|
|
5120
|
+
await insertSlackBotLifecycleSuccessAuditInScope(scopedDb, {
|
|
5121
|
+
...input,
|
|
5122
|
+
action: "slack_bot.reinstalled",
|
|
5123
|
+
connectionId: connection.id,
|
|
5124
|
+
});
|
|
5125
|
+
return connection;
|
|
4936
5126
|
});
|
|
4937
5127
|
}
|
|
4938
5128
|
|
|
5129
|
+
export async function revokeConnectionWithSlackBotSuccessAudit(
|
|
5130
|
+
db: Database,
|
|
5131
|
+
input: SlackBotLifecycleSuccessAuditInput & {
|
|
5132
|
+
connectionId: string;
|
|
5133
|
+
expectedVersion: number;
|
|
5134
|
+
},
|
|
5135
|
+
): Promise<ConnectionMetadataWithVerification | null> {
|
|
5136
|
+
return await withSlackBotLifecycleRls(db, input, async (scopedDb) => {
|
|
5137
|
+
const connection = await revokeConnectionInScope(
|
|
5138
|
+
scopedDb,
|
|
5139
|
+
input.workspaceId,
|
|
5140
|
+
input.connectionId,
|
|
5141
|
+
input.subjectId,
|
|
5142
|
+
input.expectedVersion,
|
|
5143
|
+
);
|
|
5144
|
+
if (!connection) return null;
|
|
5145
|
+
if (connection.accountId !== input.accountId) {
|
|
5146
|
+
throw new Error("Slack bot connection and lifecycle audit account must match");
|
|
5147
|
+
}
|
|
5148
|
+
await insertSlackBotLifecycleSuccessAuditInScope(scopedDb, {
|
|
5149
|
+
...input,
|
|
5150
|
+
action: "slack_bot.disconnected",
|
|
5151
|
+
connectionId: connection.id,
|
|
5152
|
+
});
|
|
5153
|
+
return connection;
|
|
5154
|
+
});
|
|
5155
|
+
}
|
|
5156
|
+
|
|
5157
|
+
export type SlackBotInstallCallbackFailureStage =
|
|
5158
|
+
| "permission_check"
|
|
5159
|
+
| "nonce_consume"
|
|
5160
|
+
| "provider_denial"
|
|
5161
|
+
| "code_exchange"
|
|
5162
|
+
| "credential_verification"
|
|
5163
|
+
| "permission_recheck"
|
|
5164
|
+
| "principal_validation"
|
|
5165
|
+
| "persistence";
|
|
5166
|
+
|
|
5167
|
+
export type SlackBotInstallCallbackFailureReason =
|
|
5168
|
+
| "permission_lost"
|
|
5169
|
+
| "state_replayed"
|
|
5170
|
+
| "provider_denied"
|
|
5171
|
+
| "missing_code"
|
|
5172
|
+
| "exchange_failed"
|
|
5173
|
+
| "scope_mismatch"
|
|
5174
|
+
| "identity_mismatch"
|
|
5175
|
+
| "credential_verification_failed"
|
|
5176
|
+
| "connection_conflict"
|
|
5177
|
+
| "principal_mismatch"
|
|
5178
|
+
| "persistence_failed"
|
|
5179
|
+
| "success_audit_failed";
|
|
5180
|
+
|
|
5181
|
+
export async function recordSlackBotInstallCallbackFailure(
|
|
5182
|
+
db: Database,
|
|
5183
|
+
input: {
|
|
5184
|
+
accountId: string;
|
|
5185
|
+
workspaceId: string;
|
|
5186
|
+
subjectId: string;
|
|
5187
|
+
callbackDigest: string;
|
|
5188
|
+
installMode: "connect" | "reinstall";
|
|
5189
|
+
stage: SlackBotInstallCallbackFailureStage;
|
|
5190
|
+
reason: SlackBotInstallCallbackFailureReason;
|
|
5191
|
+
},
|
|
5192
|
+
): Promise<boolean> {
|
|
5193
|
+
if (!/^[a-f0-9]{64}$/.test(input.callbackDigest)) {
|
|
5194
|
+
throw new Error("Slack callback digest must be a lowercase SHA-256 value");
|
|
5195
|
+
}
|
|
5196
|
+
return await withRlsContext(
|
|
5197
|
+
db,
|
|
5198
|
+
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
5199
|
+
async (scopedDb) => {
|
|
5200
|
+
await setSubjectRlsContext(scopedDb, input.subjectId);
|
|
5201
|
+
await assertWorkspaceAccountPairInScope(scopedDb, input.accountId, input.workspaceId);
|
|
5202
|
+
await scopedDb.execute(
|
|
5203
|
+
sql`select pg_advisory_xact_lock(hashtextextended(${`slack-callback-failure:${input.workspaceId}:${input.callbackDigest}`}, 0))`,
|
|
5204
|
+
);
|
|
5205
|
+
const [existing] = await scopedDb
|
|
5206
|
+
.select({ id: schema.auditEvents.id })
|
|
5207
|
+
.from(schema.auditEvents)
|
|
5208
|
+
.where(
|
|
5209
|
+
and(
|
|
5210
|
+
eq(schema.auditEvents.accountId, input.accountId),
|
|
5211
|
+
eq(schema.auditEvents.workspaceId, input.workspaceId),
|
|
5212
|
+
eq(schema.auditEvents.action, "slack_bot.install.callback.failed"),
|
|
5213
|
+
eq(schema.auditEvents.targetType, "slack_oauth_callback"),
|
|
5214
|
+
eq(schema.auditEvents.targetId, input.callbackDigest),
|
|
5215
|
+
),
|
|
5216
|
+
)
|
|
5217
|
+
.limit(1);
|
|
5218
|
+
if (existing) return false;
|
|
5219
|
+
await scopedDb.insert(schema.auditEvents).values({
|
|
5220
|
+
accountId: input.accountId,
|
|
5221
|
+
workspaceId: input.workspaceId,
|
|
5222
|
+
subjectId: input.subjectId,
|
|
5223
|
+
action: "slack_bot.install.callback.failed",
|
|
5224
|
+
targetType: "slack_oauth_callback",
|
|
5225
|
+
targetId: input.callbackDigest,
|
|
5226
|
+
metadata: {
|
|
5227
|
+
outcome: "failed",
|
|
5228
|
+
installMode: input.installMode,
|
|
5229
|
+
stage: input.stage,
|
|
5230
|
+
reason: input.reason,
|
|
5231
|
+
},
|
|
5232
|
+
});
|
|
5233
|
+
return true;
|
|
5234
|
+
},
|
|
5235
|
+
);
|
|
5236
|
+
}
|
|
5237
|
+
|
|
4939
5238
|
export type ClaimSlackBotPostOperationResult =
|
|
4940
5239
|
| { kind: "claimed" | "in_progress" | "completed"; operation: SlackBotPostOperation }
|
|
4941
5240
|
| { kind: "conflict" | "connection_not_found" };
|
|
@@ -5214,6 +5513,9 @@ export async function loadConnectionCredentialForBroker(
|
|
|
5214
5513
|
allowSubjectOwned?: boolean;
|
|
5215
5514
|
},
|
|
5216
5515
|
): Promise<ConnectionCredentialForBroker | null> {
|
|
5516
|
+
if (input.allowSubjectOwned && !input.subjectId) {
|
|
5517
|
+
return null;
|
|
5518
|
+
}
|
|
5217
5519
|
const key = environmentsEncryptionKeyBytes(settings);
|
|
5218
5520
|
if (!key) {
|
|
5219
5521
|
throw new Error(
|
|
@@ -5221,7 +5523,7 @@ export async function loadConnectionCredentialForBroker(
|
|
|
5221
5523
|
);
|
|
5222
5524
|
}
|
|
5223
5525
|
const subjectPredicate = input.allowSubjectOwned
|
|
5224
|
-
?
|
|
5526
|
+
? connectionExactSubject(input.subjectId)
|
|
5225
5527
|
: isNull(schema.connections.subjectId);
|
|
5226
5528
|
const conditions: SQL[] = [
|
|
5227
5529
|
eq(schema.connections.workspaceId, input.workspaceId),
|
|
@@ -5235,49 +5537,54 @@ export async function loadConnectionCredentialForBroker(
|
|
|
5235
5537
|
conditions.push(eq(schema.connections.kind, input.kind));
|
|
5236
5538
|
}
|
|
5237
5539
|
}
|
|
5238
|
-
return await
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
.
|
|
5245
|
-
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
)
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
|
|
5540
|
+
return await withConnectionSubjectRls(
|
|
5541
|
+
db,
|
|
5542
|
+
input.workspaceId,
|
|
5543
|
+
input.allowSubjectOwned ? input.subjectId : null,
|
|
5544
|
+
async (scopedDb) => {
|
|
5545
|
+
// Prefer active rows: a revoke bumps updatedAt, so recency alone would let a
|
|
5546
|
+
// freshly revoked connection shadow an active replacement for the provider.
|
|
5547
|
+
const [row] = await scopedDb
|
|
5548
|
+
.select()
|
|
5549
|
+
.from(schema.connections)
|
|
5550
|
+
.where(and(...conditions))
|
|
5551
|
+
.orderBy(
|
|
5552
|
+
desc(sql`(${schema.connections.status} = 'active')`),
|
|
5553
|
+
desc(schema.connections.updatedAt),
|
|
5554
|
+
)
|
|
5555
|
+
.limit(1);
|
|
5556
|
+
if (!row) {
|
|
5557
|
+
return null;
|
|
5558
|
+
}
|
|
5559
|
+
let credential: unknown;
|
|
5560
|
+
try {
|
|
5561
|
+
credential = JSON.parse(decryptEnvironmentValue(key, row.credentialEncrypted));
|
|
5562
|
+
} catch (error) {
|
|
5563
|
+
throw new Error(
|
|
5564
|
+
`connection credential could not be decrypted for ${row.id}: ${error instanceof Error ? error.message : String(error)}`,
|
|
5565
|
+
{ cause: error },
|
|
5566
|
+
);
|
|
5567
|
+
}
|
|
5568
|
+
if (!credential || typeof credential !== "object" || Array.isArray(credential)) {
|
|
5569
|
+
throw new Error(`connection credential bundle for ${row.id} is not a JSON object`);
|
|
5570
|
+
}
|
|
5571
|
+
return {
|
|
5572
|
+
id: row.id,
|
|
5573
|
+
accountId: row.accountId,
|
|
5574
|
+
workspaceId: row.workspaceId,
|
|
5575
|
+
subjectId: row.subjectId,
|
|
5576
|
+
providerDomain: row.providerDomain,
|
|
5577
|
+
kind: row.kind as ConnectionKind,
|
|
5578
|
+
status: row.status as ConnectionStatus,
|
|
5579
|
+
credential: credential as Record<string, unknown>,
|
|
5580
|
+
grantedScopes: row.grantedScopes,
|
|
5581
|
+
expiresAt: row.expiresAt,
|
|
5582
|
+
lastRefreshAt: row.lastRefreshAt,
|
|
5583
|
+
version: row.version,
|
|
5584
|
+
metadata: row.metadata,
|
|
5585
|
+
};
|
|
5586
|
+
},
|
|
5587
|
+
);
|
|
5281
5588
|
}
|
|
5282
5589
|
|
|
5283
5590
|
export async function recordConnectionTokenRefresh(
|
|
@@ -5290,35 +5597,42 @@ export async function recordConnectionTokenRefresh(
|
|
|
5290
5597
|
expiresAt: Date | null;
|
|
5291
5598
|
grantedScopes?: string[];
|
|
5292
5599
|
lastRefreshAt: Date;
|
|
5600
|
+
subjectId?: string | null;
|
|
5293
5601
|
},
|
|
5294
5602
|
): Promise<boolean> {
|
|
5295
|
-
return await
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5603
|
+
return await withConnectionSubjectRls(
|
|
5604
|
+
db,
|
|
5605
|
+
input.workspaceId,
|
|
5606
|
+
input.subjectId,
|
|
5607
|
+
async (scopedDb) => {
|
|
5608
|
+
const set = {
|
|
5609
|
+
credentialEncrypted: input.credentialEncrypted,
|
|
5610
|
+
expiresAt: input.expiresAt,
|
|
5611
|
+
lastRefreshAt: input.lastRefreshAt,
|
|
5612
|
+
status: "active",
|
|
5613
|
+
lastError: null,
|
|
5614
|
+
version: sql`${schema.connections.version} + 1`,
|
|
5615
|
+
updatedAt: new Date(),
|
|
5616
|
+
...(input.grantedScopes !== undefined ? { grantedScopes: input.grantedScopes } : {}),
|
|
5617
|
+
};
|
|
5618
|
+
const updated = await scopedDb
|
|
5619
|
+
.update(schema.connections)
|
|
5620
|
+
.set(set)
|
|
5621
|
+
.where(
|
|
5622
|
+
and(
|
|
5623
|
+
eq(schema.connections.id, input.id),
|
|
5624
|
+
eq(schema.connections.workspaceId, input.workspaceId),
|
|
5625
|
+
eq(schema.connections.version, input.version),
|
|
5626
|
+
connectionExactSubject(input.subjectId),
|
|
5627
|
+
// A refresh may only ever renew a live credential; revoked/errored rows
|
|
5628
|
+
// stay dead even if a status change somewhere forgot to bump version.
|
|
5629
|
+
eq(schema.connections.status, "active"),
|
|
5630
|
+
),
|
|
5631
|
+
)
|
|
5632
|
+
.returning({ id: schema.connections.id });
|
|
5633
|
+
return updated.length > 0;
|
|
5634
|
+
},
|
|
5635
|
+
);
|
|
5322
5636
|
}
|
|
5323
5637
|
|
|
5324
5638
|
export async function setConnectionStatus(
|
|
@@ -5326,9 +5640,9 @@ export async function setConnectionStatus(
|
|
|
5326
5640
|
workspaceId: string,
|
|
5327
5641
|
status: ConnectionStatus,
|
|
5328
5642
|
lastError: string | null,
|
|
5329
|
-
guard: { id: string; version: number },
|
|
5643
|
+
guard: { id: string; version: number; subjectId?: string | null },
|
|
5330
5644
|
): Promise<boolean> {
|
|
5331
|
-
return await
|
|
5645
|
+
return await withConnectionSubjectRls(db, workspaceId, guard.subjectId, async (scopedDb) => {
|
|
5332
5646
|
const updated = await scopedDb
|
|
5333
5647
|
.update(schema.connections)
|
|
5334
5648
|
.set({
|
|
@@ -5346,6 +5660,7 @@ export async function setConnectionStatus(
|
|
|
5346
5660
|
eq(schema.connections.workspaceId, workspaceId),
|
|
5347
5661
|
eq(schema.connections.id, guard.id),
|
|
5348
5662
|
eq(schema.connections.version, guard.version),
|
|
5663
|
+
connectionExactSubject(guard.subjectId),
|
|
5349
5664
|
),
|
|
5350
5665
|
)
|
|
5351
5666
|
.returning({ id: schema.connections.id });
|
|
@@ -5357,8 +5672,9 @@ export async function recordConnectionUsed(
|
|
|
5357
5672
|
db: Database,
|
|
5358
5673
|
workspaceId: string,
|
|
5359
5674
|
connectionId: string,
|
|
5675
|
+
subjectId?: string | null,
|
|
5360
5676
|
): Promise<void> {
|
|
5361
|
-
await
|
|
5677
|
+
await withConnectionSubjectRls(db, workspaceId, subjectId, async (scopedDb) => {
|
|
5362
5678
|
await scopedDb
|
|
5363
5679
|
.update(schema.connections)
|
|
5364
5680
|
.set({
|
|
@@ -5369,6 +5685,7 @@ export async function recordConnectionUsed(
|
|
|
5369
5685
|
and(
|
|
5370
5686
|
eq(schema.connections.workspaceId, workspaceId),
|
|
5371
5687
|
eq(schema.connections.id, connectionId),
|
|
5688
|
+
connectionExactSubject(subjectId),
|
|
5372
5689
|
),
|
|
5373
5690
|
);
|
|
5374
5691
|
});
|
|
@@ -13558,6 +13875,7 @@ export type SessionCreateInput = {
|
|
|
13558
13875
|
initialMessage: string;
|
|
13559
13876
|
initialTurnInstructions?: string | null;
|
|
13560
13877
|
resources: ResourceRef[];
|
|
13878
|
+
skills?: SessionSkill[];
|
|
13561
13879
|
tools?: ToolRef[];
|
|
13562
13880
|
toolPolicy?: SessionToolPolicy | null;
|
|
13563
13881
|
metadata: Record<string, unknown>;
|
|
@@ -13957,6 +14275,7 @@ async function createSessionInTransaction(
|
|
|
13957
14275
|
initialMessage: input.initialMessage,
|
|
13958
14276
|
initialTurnInstructions: input.initialTurnInstructions ?? null,
|
|
13959
14277
|
resources: input.resources,
|
|
14278
|
+
skills: input.skills ?? [],
|
|
13960
14279
|
tools: input.tools ?? [],
|
|
13961
14280
|
toolPolicy: input.toolPolicy ?? null,
|
|
13962
14281
|
metadata: input.metadata,
|
|
@@ -15970,13 +16289,16 @@ type LineageIdRow = {
|
|
|
15970
16289
|
parentSessionId: string | null;
|
|
15971
16290
|
depth: number;
|
|
15972
16291
|
path: string[];
|
|
16292
|
+
cycle: boolean;
|
|
15973
16293
|
};
|
|
15974
16294
|
|
|
15975
16295
|
/**
|
|
15976
16296
|
* Read the full lineage slice around a session. Every recursive step carries
|
|
15977
16297
|
* workspace_id as a hard predicate; a foreign parent/child id is invisible even
|
|
15978
|
-
* before RLS is considered.
|
|
15979
|
-
*
|
|
16298
|
+
* before RLS is considered. Up to 63 ancestors are returned root-first; an
|
|
16299
|
+
* invalid, cyclic, foreign, or deeper chain fails closed instead of presenting
|
|
16300
|
+
* a partial path as if it were rooted. Descendants are capped at depth 5 and
|
|
16301
|
+
* 200 total rows, returned as a nested tree.
|
|
15980
16302
|
*/
|
|
15981
16303
|
export async function getSessionLineage(
|
|
15982
16304
|
db: Database,
|
|
@@ -15998,25 +16320,39 @@ export async function getSessionLineage(
|
|
|
15998
16320
|
return null;
|
|
15999
16321
|
}
|
|
16000
16322
|
|
|
16001
|
-
const
|
|
16002
|
-
with recursive ancestors(id, parent_session_id, depth, path) as (
|
|
16003
|
-
select ${schema.sessions.id}, ${schema.sessions.parentSessionId}, 0, array[${schema.sessions.id}]
|
|
16323
|
+
const ancestorLineageRows = (await scopedDb.execute(sql<LineageIdRow>`
|
|
16324
|
+
with recursive ancestors(id, parent_session_id, depth, path, cycle) as (
|
|
16325
|
+
select ${schema.sessions.id}, ${schema.sessions.parentSessionId}, 0, array[${schema.sessions.id}], false
|
|
16004
16326
|
from ${schema.sessions}
|
|
16005
16327
|
where ${schema.sessions.workspaceId} = ${workspaceId}
|
|
16006
16328
|
and ${schema.sessions.id} = ${sessionId}
|
|
16007
16329
|
union all
|
|
16008
|
-
select
|
|
16330
|
+
select
|
|
16331
|
+
parent.id,
|
|
16332
|
+
parent.parent_session_id,
|
|
16333
|
+
ancestors.depth + 1,
|
|
16334
|
+
ancestors.path || parent.id,
|
|
16335
|
+
parent.id = any(ancestors.path)
|
|
16009
16336
|
from ${schema.sessions} parent
|
|
16010
16337
|
join ancestors on ancestors.parent_session_id = parent.id
|
|
16011
16338
|
where parent.workspace_id = ${workspaceId}
|
|
16012
|
-
and ancestors.
|
|
16013
|
-
and
|
|
16339
|
+
and not ancestors.cycle
|
|
16340
|
+
and ancestors.depth < 64
|
|
16014
16341
|
)
|
|
16015
|
-
select id, parent_session_id as "parentSessionId", depth, path
|
|
16342
|
+
select id, parent_session_id as "parentSessionId", depth, path, cycle
|
|
16016
16343
|
from ancestors
|
|
16017
|
-
where depth > 0
|
|
16018
16344
|
order by depth desc
|
|
16019
16345
|
`)) as LineageIdRow[];
|
|
16346
|
+
const frontier = ancestorLineageRows[0];
|
|
16347
|
+
if (
|
|
16348
|
+
!frontier ||
|
|
16349
|
+
frontier.cycle ||
|
|
16350
|
+
frontier.parentSessionId !== null ||
|
|
16351
|
+
Number(frontier.depth) >= 64
|
|
16352
|
+
) {
|
|
16353
|
+
throw new Error(`session lineage for ${sessionId} has no valid workspace root`);
|
|
16354
|
+
}
|
|
16355
|
+
const ancestorRows = ancestorLineageRows.filter((row) => row.depth > 0);
|
|
16020
16356
|
|
|
16021
16357
|
const childRows = (await scopedDb.execute(sql<LineageIdRow>`
|
|
16022
16358
|
with recursive descendants(id, parent_session_id, depth, path) as (
|
|
@@ -16032,7 +16368,7 @@ export async function getSessionLineage(
|
|
|
16032
16368
|
and descendants.depth < 5
|
|
16033
16369
|
and not child.id = any(descendants.path)
|
|
16034
16370
|
)
|
|
16035
|
-
select id, parent_session_id as "parentSessionId", depth, path
|
|
16371
|
+
select id, parent_session_id as "parentSessionId", depth, path, false as cycle
|
|
16036
16372
|
from descendants
|
|
16037
16373
|
order by path
|
|
16038
16374
|
limit ${descendantLimit + 1}
|
|
@@ -29587,6 +29923,12 @@ export async function claimSessionWorkForAttempt(
|
|
|
29587
29923
|
and(
|
|
29588
29924
|
eq(schema.sessionAttemptInterruptions.workspaceId, workspaceId),
|
|
29589
29925
|
eq(schema.sessionAttemptInterruptions.sessionId, sessionId),
|
|
29926
|
+
inArray(schema.sessionAttemptInterruptions.state, [
|
|
29927
|
+
"pending",
|
|
29928
|
+
"delivered",
|
|
29929
|
+
"acknowledged",
|
|
29930
|
+
"settled",
|
|
29931
|
+
]),
|
|
29590
29932
|
isNull(schema.sessionTurnAttempts.quiescedAt),
|
|
29591
29933
|
),
|
|
29592
29934
|
)
|
|
@@ -29878,20 +30220,27 @@ export async function claimSessionWorkForAttempt(
|
|
|
29878
30220
|
)
|
|
29879
30221
|
.limit(1)
|
|
29880
30222
|
.for("update");
|
|
29881
|
-
|
|
29882
|
-
|
|
29883
|
-
|
|
29884
|
-
|
|
29885
|
-
|
|
29886
|
-
|
|
29887
|
-
|
|
29888
|
-
|
|
29889
|
-
|
|
30223
|
+
// A human/API Steer is the newest explicit replacement direction. It
|
|
30224
|
+
// must claim next even when an older Agent Steer is pending; the
|
|
30225
|
+
// internal instruction is delivered as context on that same human turn
|
|
30226
|
+
// instead of manufacturing another system inference ahead of it.
|
|
30227
|
+
// Ordinary queued sends retain the established Agent-Steer priority.
|
|
30228
|
+
const rows = await rawRows<{
|
|
30229
|
+
id: string;
|
|
30230
|
+
trigger_event_id: string;
|
|
30231
|
+
metadata: Record<string, unknown>;
|
|
30232
|
+
}>(
|
|
30233
|
+
tx as unknown as Database,
|
|
30234
|
+
sql`select id, trigger_event_id, metadata from session_turns
|
|
29890
30235
|
where workspace_id = ${workspaceId} and session_id = ${sessionId}
|
|
29891
30236
|
and status = 'queued' and source in ('user', 'api')
|
|
30237
|
+
and (
|
|
30238
|
+
${Boolean(pendingAgentSteer)} = false
|
|
30239
|
+
or metadata->>'delivery' = 'steer'
|
|
30240
|
+
)
|
|
29892
30241
|
order by position asc, created_at asc, id asc
|
|
29893
30242
|
limit 1`,
|
|
29894
|
-
|
|
30243
|
+
);
|
|
29895
30244
|
const queuedTurnPreview = rows[0];
|
|
29896
30245
|
const queuedLocks = queuedTurnPreview
|
|
29897
30246
|
? await lockSessionEventWriteRows(tx as unknown as Database, {
|
|
@@ -33454,7 +33803,9 @@ export async function getSessionQueueSnapshot(
|
|
|
33454
33803
|
version: session.queueVersion,
|
|
33455
33804
|
effectiveControl: serializeEffectiveSessionControl(effectiveControl),
|
|
33456
33805
|
stoppingPreviousAttempt:
|
|
33457
|
-
latestInterruption !== null &&
|
|
33806
|
+
latestInterruption !== null &&
|
|
33807
|
+
latestInterruption.interruptionState !== "rejected_stale" &&
|
|
33808
|
+
latestInterruption.quiescedAt === null,
|
|
33458
33809
|
items: rows.map(mapSessionTurn),
|
|
33459
33810
|
};
|
|
33460
33811
|
});
|
|
@@ -35209,6 +35560,7 @@ function mapSession(
|
|
|
35209
35560
|
titleSource: (row.titleSource as "user" | "agent" | null) ?? null,
|
|
35210
35561
|
instructions: row.instructions ?? null,
|
|
35211
35562
|
resources: row.resources as ResourceRef[],
|
|
35563
|
+
skills: (row.skills as SessionSkill[]) ?? [],
|
|
35212
35564
|
tools: row.tools as ToolRef[],
|
|
35213
35565
|
toolPolicy: (row.toolPolicy as SessionToolPolicy | null) ?? {
|
|
35214
35566
|
mode: "legacy",
|