@opengeni/db 0.13.1 → 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 +396 -172
- package/dist/index.js.map +1 -1
- package/dist/provision-roles.d.ts +43 -6
- package/dist/{schema-D-hyrsFt.d.ts → schema-SbaSoRdr.d.ts} +19 -0
- 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 +552 -201
- 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
|
};
|
|
@@ -4772,6 +4789,52 @@ function connectionSubjectVisibility(subjectId?: string | null): SQL {
|
|
|
4772
4789
|
: isNull(schema.connections.subjectId);
|
|
4773
4790
|
}
|
|
4774
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
|
+
|
|
4775
4838
|
export async function createConnection(
|
|
4776
4839
|
db: Database,
|
|
4777
4840
|
input: CreateConnectionInput,
|
|
@@ -4780,29 +4843,10 @@ export async function createConnection(
|
|
|
4780
4843
|
db,
|
|
4781
4844
|
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
4782
4845
|
async (scopedDb) => {
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
.values({
|
|
4786
|
-
accountId: input.accountId,
|
|
4787
|
-
workspaceId: input.workspaceId,
|
|
4788
|
-
subjectId: input.subjectId ?? null,
|
|
4789
|
-
providerDomain: input.providerDomain,
|
|
4790
|
-
kind: input.kind,
|
|
4791
|
-
status: input.status ?? "active",
|
|
4792
|
-
credentialEncrypted: input.credentialEncrypted,
|
|
4793
|
-
grantedScopes: input.grantedScopes ?? [],
|
|
4794
|
-
expiresAt: input.expiresAt ?? null,
|
|
4795
|
-
verifiedInstallAt: input.verifiedInstallAt ?? null,
|
|
4796
|
-
verifiedInstallVersion: input.verifiedInstallVersion ?? null,
|
|
4797
|
-
metadata: input.metadata ?? {},
|
|
4798
|
-
createdBySubjectId: input.createdBySubjectId ?? null,
|
|
4799
|
-
updatedBySubjectId: input.updatedBySubjectId ?? input.createdBySubjectId ?? null,
|
|
4800
|
-
})
|
|
4801
|
-
.returning(connectionMetadataColumns);
|
|
4802
|
-
if (!row) {
|
|
4803
|
-
throw new Error("Failed to create connection");
|
|
4846
|
+
if (input.subjectId) {
|
|
4847
|
+
await setSubjectRlsContext(scopedDb, input.subjectId);
|
|
4804
4848
|
}
|
|
4805
|
-
return
|
|
4849
|
+
return await createConnectionInScope(scopedDb, input);
|
|
4806
4850
|
},
|
|
4807
4851
|
);
|
|
4808
4852
|
}
|
|
@@ -4812,7 +4856,7 @@ export async function listConnectionsMetadata(
|
|
|
4812
4856
|
workspaceId: string,
|
|
4813
4857
|
subjectId?: string | null,
|
|
4814
4858
|
): Promise<ConnectionMetadataWithVerification[]> {
|
|
4815
|
-
return await
|
|
4859
|
+
return await withConnectionSubjectRls(db, workspaceId, subjectId, async (scopedDb) => {
|
|
4816
4860
|
const rows = await scopedDb
|
|
4817
4861
|
.select(connectionMetadataColumns)
|
|
4818
4862
|
.from(schema.connections)
|
|
@@ -4833,7 +4877,7 @@ export async function getConnectionMetadata(
|
|
|
4833
4877
|
connectionId: string,
|
|
4834
4878
|
subjectId?: string | null,
|
|
4835
4879
|
): Promise<ConnectionMetadataWithVerification | null> {
|
|
4836
|
-
return await
|
|
4880
|
+
return await withConnectionSubjectRls(db, workspaceId, subjectId, async (scopedDb) => {
|
|
4837
4881
|
const [row] = await scopedDb
|
|
4838
4882
|
.select(connectionMetadataColumns)
|
|
4839
4883
|
.from(schema.connections)
|
|
@@ -4849,53 +4893,102 @@ export async function getConnectionMetadata(
|
|
|
4849
4893
|
});
|
|
4850
4894
|
}
|
|
4851
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
|
+
|
|
4852
4943
|
export async function updateConnection(
|
|
4853
4944
|
db: Database,
|
|
4854
4945
|
input: UpdateConnectionInput,
|
|
4855
4946
|
): Promise<ConnectionMetadataWithVerification | null> {
|
|
4856
|
-
return await
|
|
4857
|
-
|
|
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,
|
|
4858
4977
|
updatedAt: new Date(),
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
? { verifiedInstallAt: input.verifiedInstallAt }
|
|
4874
|
-
: {}),
|
|
4875
|
-
...(input.verifiedInstallVersion !== undefined
|
|
4876
|
-
? { verifiedInstallVersion: input.verifiedInstallVersion }
|
|
4877
|
-
: {}),
|
|
4878
|
-
...(input.metadata !== undefined ? { metadata: input.metadata } : {}),
|
|
4879
|
-
...(input.updatedBySubjectId !== undefined
|
|
4880
|
-
? { updatedBySubjectId: input.updatedBySubjectId }
|
|
4881
|
-
: {}),
|
|
4882
|
-
};
|
|
4883
|
-
const [row] = await scopedDb
|
|
4884
|
-
.update(schema.connections)
|
|
4885
|
-
.set(set)
|
|
4886
|
-
.where(
|
|
4887
|
-
and(
|
|
4888
|
-
eq(schema.connections.workspaceId, input.workspaceId),
|
|
4889
|
-
eq(schema.connections.id, input.connectionId),
|
|
4890
|
-
connectionSubjectVisibility(input.visibleToSubjectId),
|
|
4891
|
-
...(input.expectedVersion !== undefined
|
|
4892
|
-
? [eq(schema.connections.version, input.expectedVersion)]
|
|
4893
|
-
: []),
|
|
4894
|
-
),
|
|
4895
|
-
)
|
|
4896
|
-
.returning(connectionMetadataColumns);
|
|
4897
|
-
return row ? mapConnectionMetadata(row) : null;
|
|
4898
|
-
});
|
|
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;
|
|
4899
4992
|
}
|
|
4900
4993
|
|
|
4901
4994
|
export async function revokeConnection(
|
|
@@ -4904,39 +4997,244 @@ export async function revokeConnection(
|
|
|
4904
4997
|
connectionId: string,
|
|
4905
4998
|
updatedBySubjectId?: string | null,
|
|
4906
4999
|
): Promise<ConnectionMetadataWithVerification | null> {
|
|
4907
|
-
return await
|
|
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
|
-
|
|
4936
|
-
|
|
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;
|
|
4937
5126
|
});
|
|
4938
5127
|
}
|
|
4939
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
|
+
|
|
4940
5238
|
export type ClaimSlackBotPostOperationResult =
|
|
4941
5239
|
| { kind: "claimed" | "in_progress" | "completed"; operation: SlackBotPostOperation }
|
|
4942
5240
|
| { kind: "conflict" | "connection_not_found" };
|
|
@@ -5215,6 +5513,9 @@ export async function loadConnectionCredentialForBroker(
|
|
|
5215
5513
|
allowSubjectOwned?: boolean;
|
|
5216
5514
|
},
|
|
5217
5515
|
): Promise<ConnectionCredentialForBroker | null> {
|
|
5516
|
+
if (input.allowSubjectOwned && !input.subjectId) {
|
|
5517
|
+
return null;
|
|
5518
|
+
}
|
|
5218
5519
|
const key = environmentsEncryptionKeyBytes(settings);
|
|
5219
5520
|
if (!key) {
|
|
5220
5521
|
throw new Error(
|
|
@@ -5222,7 +5523,7 @@ export async function loadConnectionCredentialForBroker(
|
|
|
5222
5523
|
);
|
|
5223
5524
|
}
|
|
5224
5525
|
const subjectPredicate = input.allowSubjectOwned
|
|
5225
|
-
?
|
|
5526
|
+
? connectionExactSubject(input.subjectId)
|
|
5226
5527
|
: isNull(schema.connections.subjectId);
|
|
5227
5528
|
const conditions: SQL[] = [
|
|
5228
5529
|
eq(schema.connections.workspaceId, input.workspaceId),
|
|
@@ -5236,49 +5537,54 @@ export async function loadConnectionCredentialForBroker(
|
|
|
5236
5537
|
conditions.push(eq(schema.connections.kind, input.kind));
|
|
5237
5538
|
}
|
|
5238
5539
|
}
|
|
5239
|
-
return await
|
|
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
|
-
|
|
5281
|
-
|
|
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
|
+
);
|
|
5282
5588
|
}
|
|
5283
5589
|
|
|
5284
5590
|
export async function recordConnectionTokenRefresh(
|
|
@@ -5291,35 +5597,42 @@ export async function recordConnectionTokenRefresh(
|
|
|
5291
5597
|
expiresAt: Date | null;
|
|
5292
5598
|
grantedScopes?: string[];
|
|
5293
5599
|
lastRefreshAt: Date;
|
|
5600
|
+
subjectId?: string | null;
|
|
5294
5601
|
},
|
|
5295
5602
|
): Promise<boolean> {
|
|
5296
|
-
return await
|
|
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
|
-
|
|
5322
|
-
|
|
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
|
+
);
|
|
5323
5636
|
}
|
|
5324
5637
|
|
|
5325
5638
|
export async function setConnectionStatus(
|
|
@@ -5327,9 +5640,9 @@ export async function setConnectionStatus(
|
|
|
5327
5640
|
workspaceId: string,
|
|
5328
5641
|
status: ConnectionStatus,
|
|
5329
5642
|
lastError: string | null,
|
|
5330
|
-
guard: { id: string; version: number },
|
|
5643
|
+
guard: { id: string; version: number; subjectId?: string | null },
|
|
5331
5644
|
): Promise<boolean> {
|
|
5332
|
-
return await
|
|
5645
|
+
return await withConnectionSubjectRls(db, workspaceId, guard.subjectId, async (scopedDb) => {
|
|
5333
5646
|
const updated = await scopedDb
|
|
5334
5647
|
.update(schema.connections)
|
|
5335
5648
|
.set({
|
|
@@ -5347,6 +5660,7 @@ export async function setConnectionStatus(
|
|
|
5347
5660
|
eq(schema.connections.workspaceId, workspaceId),
|
|
5348
5661
|
eq(schema.connections.id, guard.id),
|
|
5349
5662
|
eq(schema.connections.version, guard.version),
|
|
5663
|
+
connectionExactSubject(guard.subjectId),
|
|
5350
5664
|
),
|
|
5351
5665
|
)
|
|
5352
5666
|
.returning({ id: schema.connections.id });
|
|
@@ -5358,8 +5672,9 @@ export async function recordConnectionUsed(
|
|
|
5358
5672
|
db: Database,
|
|
5359
5673
|
workspaceId: string,
|
|
5360
5674
|
connectionId: string,
|
|
5675
|
+
subjectId?: string | null,
|
|
5361
5676
|
): Promise<void> {
|
|
5362
|
-
await
|
|
5677
|
+
await withConnectionSubjectRls(db, workspaceId, subjectId, async (scopedDb) => {
|
|
5363
5678
|
await scopedDb
|
|
5364
5679
|
.update(schema.connections)
|
|
5365
5680
|
.set({
|
|
@@ -5370,6 +5685,7 @@ export async function recordConnectionUsed(
|
|
|
5370
5685
|
and(
|
|
5371
5686
|
eq(schema.connections.workspaceId, workspaceId),
|
|
5372
5687
|
eq(schema.connections.id, connectionId),
|
|
5688
|
+
connectionExactSubject(subjectId),
|
|
5373
5689
|
),
|
|
5374
5690
|
);
|
|
5375
5691
|
});
|
|
@@ -13559,6 +13875,7 @@ export type SessionCreateInput = {
|
|
|
13559
13875
|
initialMessage: string;
|
|
13560
13876
|
initialTurnInstructions?: string | null;
|
|
13561
13877
|
resources: ResourceRef[];
|
|
13878
|
+
skills?: SessionSkill[];
|
|
13562
13879
|
tools?: ToolRef[];
|
|
13563
13880
|
toolPolicy?: SessionToolPolicy | null;
|
|
13564
13881
|
metadata: Record<string, unknown>;
|
|
@@ -13958,6 +14275,7 @@ async function createSessionInTransaction(
|
|
|
13958
14275
|
initialMessage: input.initialMessage,
|
|
13959
14276
|
initialTurnInstructions: input.initialTurnInstructions ?? null,
|
|
13960
14277
|
resources: input.resources,
|
|
14278
|
+
skills: input.skills ?? [],
|
|
13961
14279
|
tools: input.tools ?? [],
|
|
13962
14280
|
toolPolicy: input.toolPolicy ?? null,
|
|
13963
14281
|
metadata: input.metadata,
|
|
@@ -15971,13 +16289,16 @@ type LineageIdRow = {
|
|
|
15971
16289
|
parentSessionId: string | null;
|
|
15972
16290
|
depth: number;
|
|
15973
16291
|
path: string[];
|
|
16292
|
+
cycle: boolean;
|
|
15974
16293
|
};
|
|
15975
16294
|
|
|
15976
16295
|
/**
|
|
15977
16296
|
* Read the full lineage slice around a session. Every recursive step carries
|
|
15978
16297
|
* workspace_id as a hard predicate; a foreign parent/child id is invisible even
|
|
15979
|
-
* before RLS is considered.
|
|
15980
|
-
*
|
|
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.
|
|
15981
16302
|
*/
|
|
15982
16303
|
export async function getSessionLineage(
|
|
15983
16304
|
db: Database,
|
|
@@ -15999,25 +16320,39 @@ export async function getSessionLineage(
|
|
|
15999
16320
|
return null;
|
|
16000
16321
|
}
|
|
16001
16322
|
|
|
16002
|
-
const
|
|
16003
|
-
with recursive ancestors(id, parent_session_id, depth, path) as (
|
|
16004
|
-
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
|
|
16005
16326
|
from ${schema.sessions}
|
|
16006
16327
|
where ${schema.sessions.workspaceId} = ${workspaceId}
|
|
16007
16328
|
and ${schema.sessions.id} = ${sessionId}
|
|
16008
16329
|
union all
|
|
16009
|
-
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)
|
|
16010
16336
|
from ${schema.sessions} parent
|
|
16011
16337
|
join ancestors on ancestors.parent_session_id = parent.id
|
|
16012
16338
|
where parent.workspace_id = ${workspaceId}
|
|
16013
|
-
and ancestors.
|
|
16014
|
-
and
|
|
16339
|
+
and not ancestors.cycle
|
|
16340
|
+
and ancestors.depth < 64
|
|
16015
16341
|
)
|
|
16016
|
-
select id, parent_session_id as "parentSessionId", depth, path
|
|
16342
|
+
select id, parent_session_id as "parentSessionId", depth, path, cycle
|
|
16017
16343
|
from ancestors
|
|
16018
|
-
where depth > 0
|
|
16019
16344
|
order by depth desc
|
|
16020
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);
|
|
16021
16356
|
|
|
16022
16357
|
const childRows = (await scopedDb.execute(sql<LineageIdRow>`
|
|
16023
16358
|
with recursive descendants(id, parent_session_id, depth, path) as (
|
|
@@ -16033,7 +16368,7 @@ export async function getSessionLineage(
|
|
|
16033
16368
|
and descendants.depth < 5
|
|
16034
16369
|
and not child.id = any(descendants.path)
|
|
16035
16370
|
)
|
|
16036
|
-
select id, parent_session_id as "parentSessionId", depth, path
|
|
16371
|
+
select id, parent_session_id as "parentSessionId", depth, path, false as cycle
|
|
16037
16372
|
from descendants
|
|
16038
16373
|
order by path
|
|
16039
16374
|
limit ${descendantLimit + 1}
|
|
@@ -29588,6 +29923,12 @@ export async function claimSessionWorkForAttempt(
|
|
|
29588
29923
|
and(
|
|
29589
29924
|
eq(schema.sessionAttemptInterruptions.workspaceId, workspaceId),
|
|
29590
29925
|
eq(schema.sessionAttemptInterruptions.sessionId, sessionId),
|
|
29926
|
+
inArray(schema.sessionAttemptInterruptions.state, [
|
|
29927
|
+
"pending",
|
|
29928
|
+
"delivered",
|
|
29929
|
+
"acknowledged",
|
|
29930
|
+
"settled",
|
|
29931
|
+
]),
|
|
29591
29932
|
isNull(schema.sessionTurnAttempts.quiescedAt),
|
|
29592
29933
|
),
|
|
29593
29934
|
)
|
|
@@ -29879,20 +30220,27 @@ export async function claimSessionWorkForAttempt(
|
|
|
29879
30220
|
)
|
|
29880
30221
|
.limit(1)
|
|
29881
30222
|
.for("update");
|
|
29882
|
-
|
|
29883
|
-
|
|
29884
|
-
|
|
29885
|
-
|
|
29886
|
-
|
|
29887
|
-
|
|
29888
|
-
|
|
29889
|
-
|
|
29890
|
-
|
|
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
|
|
29891
30235
|
where workspace_id = ${workspaceId} and session_id = ${sessionId}
|
|
29892
30236
|
and status = 'queued' and source in ('user', 'api')
|
|
30237
|
+
and (
|
|
30238
|
+
${Boolean(pendingAgentSteer)} = false
|
|
30239
|
+
or metadata->>'delivery' = 'steer'
|
|
30240
|
+
)
|
|
29893
30241
|
order by position asc, created_at asc, id asc
|
|
29894
30242
|
limit 1`,
|
|
29895
|
-
|
|
30243
|
+
);
|
|
29896
30244
|
const queuedTurnPreview = rows[0];
|
|
29897
30245
|
const queuedLocks = queuedTurnPreview
|
|
29898
30246
|
? await lockSessionEventWriteRows(tx as unknown as Database, {
|
|
@@ -33455,7 +33803,9 @@ export async function getSessionQueueSnapshot(
|
|
|
33455
33803
|
version: session.queueVersion,
|
|
33456
33804
|
effectiveControl: serializeEffectiveSessionControl(effectiveControl),
|
|
33457
33805
|
stoppingPreviousAttempt:
|
|
33458
|
-
latestInterruption !== null &&
|
|
33806
|
+
latestInterruption !== null &&
|
|
33807
|
+
latestInterruption.interruptionState !== "rejected_stale" &&
|
|
33808
|
+
latestInterruption.quiescedAt === null,
|
|
33459
33809
|
items: rows.map(mapSessionTurn),
|
|
33460
33810
|
};
|
|
33461
33811
|
});
|
|
@@ -35210,6 +35560,7 @@ function mapSession(
|
|
|
35210
35560
|
titleSource: (row.titleSource as "user" | "agent" | null) ?? null,
|
|
35211
35561
|
instructions: row.instructions ?? null,
|
|
35212
35562
|
resources: row.resources as ResourceRef[],
|
|
35563
|
+
skills: (row.skills as SessionSkill[]) ?? [],
|
|
35213
35564
|
tools: row.tools as ToolRef[],
|
|
35214
35565
|
toolPolicy: (row.toolPolicy as SessionToolPolicy | null) ?? {
|
|
35215
35566
|
mode: "legacy",
|