mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-10-25 11:47:32 +00:00
🩻 feat(insights): Scope Insights to Authorized Agents (#15549)
* feat(insights): add agent-scoped access * fix(insights): exclude unattributed assistant messages * fix(sharing): key role insight toggles by principal * fix(sharing): preserve insight grant snapshot * fix(permissions): preserve insight bits atomically * fix(permissions): guard insight audit rollback * fix(insights): keep admin grant controls accessible * fix(permissions): reconcile duplicate principal updates * fix(insights): show automatic admin access * fix(permissions): harden insights access updates * fix(insights): simplify authorization and preserve client state
This commit is contained in:
parent
57c52de33c
commit
000349681e
68 changed files with 3682 additions and 747 deletions
|
|
@ -5,9 +5,18 @@
|
|||
const mongoose = require('mongoose');
|
||||
const { logger, getTenantId, SYSTEM_TENANT_ID } = require('@librechat/data-schemas');
|
||||
const { ResourceType, PrincipalType, PermissionBits } = require('librechat-data-provider');
|
||||
const { enrichRemoteAgentPrincipals, backfillRemoteAgentPermissions } = require('@librechat/api');
|
||||
const {
|
||||
enrichRemoteAgentPrincipals,
|
||||
backfillRemoteAgentPermissions,
|
||||
auditInsightsPermissionChanges,
|
||||
getInsightsPrincipalState,
|
||||
maskAgentInsightsBit,
|
||||
sanitizeInsightsPermissionPrincipals,
|
||||
validateInsightsPermissionUpdates,
|
||||
} = require('@librechat/api');
|
||||
const {
|
||||
bulkUpdateResourcePermissions,
|
||||
restoreInsightsPermissionChanges,
|
||||
ensureGroupPrincipalExists,
|
||||
getResourcePermissionsMap,
|
||||
findAccessibleResources,
|
||||
|
|
@ -65,14 +74,24 @@ const updateResourcePermissions = async (req, res) => {
|
|||
/** @type {TUpdateResourcePermissionsRequest} */
|
||||
const { updated, removed, public: isPublic, publicAccessRoleId } = req.body;
|
||||
const { id: userId } = req.user;
|
||||
const updatedList = Array.isArray(updated) ? updated : [];
|
||||
const removedList = Array.isArray(removed) ? removed : [];
|
||||
const insightsValidation = validateInsightsPermissionUpdates({
|
||||
resourceType,
|
||||
userRole: req.user.role,
|
||||
updatedPrincipals: updatedList,
|
||||
});
|
||||
if (insightsValidation) {
|
||||
return res.status(insightsValidation.status).json({ error: insightsValidation.error });
|
||||
}
|
||||
|
||||
// Prepare principals for the service call
|
||||
const updatedPrincipals = [];
|
||||
const revokedPrincipals = [];
|
||||
|
||||
// Add updated principals
|
||||
if (updated && Array.isArray(updated)) {
|
||||
updatedPrincipals.push(...updated);
|
||||
if (updatedList.length > 0) {
|
||||
updatedPrincipals.push(...updatedList);
|
||||
}
|
||||
|
||||
// Add public permission if enabled
|
||||
|
|
@ -138,8 +157,8 @@ const updateResourcePermissions = async (req, res) => {
|
|||
}
|
||||
|
||||
// Add removed principals
|
||||
if (removed && Array.isArray(removed)) {
|
||||
revokedPrincipals.push(...removed);
|
||||
if (removedList.length > 0) {
|
||||
revokedPrincipals.push(...removedList);
|
||||
}
|
||||
|
||||
// If public is explicitly disabled, add public to revoked list
|
||||
|
|
@ -158,6 +177,24 @@ const updateResourcePermissions = async (req, res) => {
|
|||
grantedBy: userId,
|
||||
});
|
||||
|
||||
await auditInsightsPermissionChanges({
|
||||
req,
|
||||
resourceId,
|
||||
changes: results.insightsChanges ?? [],
|
||||
failClosed: process.env.AUDIT_LOG_FAIL_CLOSED === 'true',
|
||||
deps: {
|
||||
getAgent: db.getAgent,
|
||||
recordAuditEntry: db.recordAuditEntry,
|
||||
restoreInsightsPermissionChanges: (changes) =>
|
||||
restoreInsightsPermissionChanges({
|
||||
resourceType: ResourceType.AGENT,
|
||||
resourceId,
|
||||
changes,
|
||||
}),
|
||||
logger,
|
||||
},
|
||||
});
|
||||
|
||||
if (resourceType === ResourceType.CODE_ENVIRONMENT) {
|
||||
await invalidateCodeEnvironmentConfigCache(req.user.tenantId).catch((error) => {
|
||||
// Cached environment metadata is authorization-filtered against the live ACL on every
|
||||
|
|
@ -179,10 +216,15 @@ const updateResourcePermissions = async (req, res) => {
|
|||
}
|
||||
|
||||
/** @type {TUpdateResourcePermissionsResponse} */
|
||||
const responsePrincipals = sanitizeInsightsPermissionPrincipals({
|
||||
resourceType,
|
||||
userRole: req.user.role,
|
||||
principals: results.granted,
|
||||
});
|
||||
const response = {
|
||||
message: 'Permissions updated successfully',
|
||||
results: {
|
||||
principals: results.granted,
|
||||
principals: responsePrincipals,
|
||||
...(isPublic !== undefined ? { public: isPublic } : {}),
|
||||
publicAccessRoleId: isPublic ? publicAccessRoleId : undefined,
|
||||
},
|
||||
|
|
@ -191,7 +233,7 @@ const updateResourcePermissions = async (req, res) => {
|
|||
res.status(200).json(response);
|
||||
} catch (error) {
|
||||
logger.error('Error updating resource permissions:', error);
|
||||
res.status(400).json({
|
||||
res.status(error.statusCode ?? 400).json({
|
||||
error: 'Failed to update permissions',
|
||||
details: error.message,
|
||||
});
|
||||
|
|
@ -254,6 +296,7 @@ const getResourcePermissions = async (req, res) => {
|
|||
accessRoleId: { $arrayElemAt: ['$role.accessRoleId', 0] },
|
||||
userInfo: { $arrayElemAt: ['$userInfo', 0] },
|
||||
groupInfo: { $arrayElemAt: ['$groupInfo', 0] },
|
||||
permBits: 1,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
|
@ -281,6 +324,12 @@ const getResourcePermissions = async (req, res) => {
|
|||
source: !result.userInfo._id ? 'entra' : 'local',
|
||||
idOnTheSource: result.userInfo.idOnTheSource || result.userInfo._id.toString(),
|
||||
accessRoleId: result.accessRoleId,
|
||||
...getInsightsPrincipalState({
|
||||
principalType: PrincipalType.USER,
|
||||
principalRole: result.userInfo.role,
|
||||
requesterRole: req.user.role,
|
||||
permBits: result.permBits,
|
||||
}),
|
||||
});
|
||||
} else if (
|
||||
result.principalType === PrincipalType.GROUP &&
|
||||
|
|
@ -297,6 +346,11 @@ const getResourcePermissions = async (req, res) => {
|
|||
source: result.groupInfo.source || 'local',
|
||||
idOnTheSource: result.groupInfo.idOnTheSource || result.groupInfo._id.toString(),
|
||||
accessRoleId: result.accessRoleId,
|
||||
...getInsightsPrincipalState({
|
||||
principalType: PrincipalType.GROUP,
|
||||
requesterRole: req.user.role,
|
||||
permBits: result.permBits,
|
||||
}),
|
||||
});
|
||||
} else if (result.principalType === PrincipalType.ROLE) {
|
||||
principals.push({
|
||||
|
|
@ -307,6 +361,12 @@ const getResourcePermissions = async (req, res) => {
|
|||
name: result.principalId,
|
||||
description: `System role: ${result.principalId}`,
|
||||
accessRoleId: result.accessRoleId,
|
||||
...getInsightsPrincipalState({
|
||||
principalType: PrincipalType.ROLE,
|
||||
principalRole: result.principalId,
|
||||
requesterRole: req.user.role,
|
||||
permBits: result.permBits,
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -391,7 +451,11 @@ const getUserEffectivePermissions = async (req, res) => {
|
|||
});
|
||||
|
||||
res.status(200).json({
|
||||
permissionBits,
|
||||
permissionBits: maskAgentInsightsBit({
|
||||
resourceType,
|
||||
userRole: req.user.role,
|
||||
permBits: permissionBits,
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error getting user effective permissions:', error);
|
||||
|
|
@ -556,7 +620,11 @@ const getAllEffectivePermissions = async (req, res) => {
|
|||
// Convert Map to plain object for JSON response
|
||||
const result = {};
|
||||
for (const [resourceId, permBits] of permissionsMap) {
|
||||
result[resourceId] = permBits;
|
||||
result[resourceId] = maskAgentInsightsBit({
|
||||
resourceType,
|
||||
userRole: req.user.role,
|
||||
permBits,
|
||||
});
|
||||
}
|
||||
|
||||
res.status(200).json(result);
|
||||
|
|
|
|||
|
|
@ -9,17 +9,21 @@ jest.mock('@librechat/data-schemas', () => ({
|
|||
SYSTEM_TENANT_ID: '__SYSTEM__',
|
||||
}));
|
||||
|
||||
const { AccessRoleIds, ResourceType, PrincipalType } =
|
||||
const { AccessRoleIds, ResourceType, PrincipalType, SystemRoles } =
|
||||
jest.requireActual('librechat-data-provider');
|
||||
|
||||
jest.mock('librechat-data-provider', () => ({
|
||||
...jest.requireActual('librechat-data-provider'),
|
||||
}));
|
||||
|
||||
jest.mock('@librechat/api', () => ({
|
||||
enrichRemoteAgentPrincipals: jest.fn(),
|
||||
backfillRemoteAgentPermissions: jest.fn(),
|
||||
}));
|
||||
jest.mock('@librechat/api', () => {
|
||||
const actual = jest.requireActual('@librechat/api');
|
||||
return {
|
||||
...actual,
|
||||
enrichRemoteAgentPrincipals: jest.fn(),
|
||||
backfillRemoteAgentPermissions: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const mockInvalidateCodeEnvironmentConfigCache = jest.fn().mockResolvedValue(undefined);
|
||||
jest.mock('~/server/services/Config', () => ({
|
||||
|
|
@ -28,9 +32,11 @@ jest.mock('~/server/services/Config', () => ({
|
|||
}));
|
||||
|
||||
const mockBulkUpdateResourcePermissions = jest.fn();
|
||||
const mockRestoreInsightsPermissionChanges = jest.fn();
|
||||
|
||||
jest.mock('~/server/services/PermissionService', () => ({
|
||||
bulkUpdateResourcePermissions: (...args) => mockBulkUpdateResourcePermissions(...args),
|
||||
restoreInsightsPermissionChanges: (...args) => mockRestoreInsightsPermissionChanges(...args),
|
||||
ensureGroupPrincipalExists: jest.fn(),
|
||||
getEffectivePermissions: jest.fn(),
|
||||
ensurePrincipalExists: jest.fn(),
|
||||
|
|
@ -40,6 +46,7 @@ jest.mock('~/server/services/PermissionService', () => ({
|
|||
}));
|
||||
|
||||
const mockRemoveAgentFromUserFavorites = jest.fn();
|
||||
const mockRecordAuditEntry = jest.fn();
|
||||
|
||||
jest.mock('~/models', () => ({
|
||||
aggregateAclEntries: jest.fn(),
|
||||
|
|
@ -47,6 +54,8 @@ jest.mock('~/models', () => ({
|
|||
sortPrincipalsByRelevance: jest.fn(),
|
||||
calculateRelevanceScore: jest.fn(),
|
||||
removeAgentFromUserFavorites: (...args) => mockRemoveAgentFromUserFavorites(...args),
|
||||
getAgent: jest.fn(),
|
||||
recordAuditEntry: (...args) => mockRecordAuditEntry(...args),
|
||||
}));
|
||||
|
||||
jest.mock('~/server/services/GraphApiService', () => ({
|
||||
|
|
@ -221,6 +230,7 @@ describe('PermissionsController', () => {
|
|||
name: 'Current User',
|
||||
email: 'current-user@example.com',
|
||||
avatar: 'current-user.png',
|
||||
role: SystemRoles.ADMIN,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -247,6 +257,7 @@ describe('PermissionsController', () => {
|
|||
type: PrincipalType.USER,
|
||||
id: userId.toString(),
|
||||
email: 'current-user@example.com',
|
||||
isAdmin: true,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
type: PrincipalType.GROUP,
|
||||
|
|
@ -262,6 +273,7 @@ describe('PermissionsController', () => {
|
|||
const revokedUserId = new mongoose.Types.ObjectId().toString();
|
||||
|
||||
beforeEach(() => {
|
||||
delete process.env.AUDIT_LOG_FAIL_CLOSED;
|
||||
mockBulkUpdateResourcePermissions.mockResolvedValue({
|
||||
granted: [],
|
||||
updated: [],
|
||||
|
|
@ -270,6 +282,210 @@ describe('PermissionsController', () => {
|
|||
});
|
||||
|
||||
mockRemoveAgentFromUserFavorites.mockResolvedValue(undefined);
|
||||
db.getAgent.mockResolvedValue({ _id: agentObjectId, id: 'agent-a', name: 'Agent A' });
|
||||
mockRecordAuditEntry.mockResolvedValue({});
|
||||
mockRestoreInsightsPermissionChanges.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it('rejects Insights permission changes from non-admin users', async () => {
|
||||
const req = createMockReq({
|
||||
params: { resourceType: ResourceType.AGENT, resourceId: agentObjectId },
|
||||
body: {
|
||||
updated: [
|
||||
{
|
||||
type: PrincipalType.ROLE,
|
||||
id: 'USER',
|
||||
accessRoleId: AccessRoleIds.AGENT_VIEWER,
|
||||
viewInsights: true,
|
||||
},
|
||||
],
|
||||
removed: [],
|
||||
},
|
||||
});
|
||||
const res = createMockRes();
|
||||
|
||||
await updateResourcePermissions(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(403);
|
||||
expect(mockBulkUpdateResourcePermissions).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects non-boolean Insights permission values', async () => {
|
||||
const req = createMockReq({
|
||||
params: { resourceType: ResourceType.AGENT, resourceId: agentObjectId },
|
||||
body: {
|
||||
updated: [
|
||||
{
|
||||
type: PrincipalType.ROLE,
|
||||
id: 'USER',
|
||||
accessRoleId: AccessRoleIds.AGENT_VIEWER,
|
||||
viewInsights: null,
|
||||
},
|
||||
],
|
||||
removed: [],
|
||||
},
|
||||
user: { id: 'admin-id', role: SystemRoles.ADMIN },
|
||||
});
|
||||
const res = createMockRes();
|
||||
|
||||
await updateResourcePermissions(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(400);
|
||||
expect(mockBulkUpdateResourcePermissions).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('audits each actual Insights access transition made by an admin', async () => {
|
||||
mockBulkUpdateResourcePermissions.mockResolvedValue({
|
||||
granted: [
|
||||
{
|
||||
type: PrincipalType.ROLE,
|
||||
id: 'USER',
|
||||
accessRoleId: AccessRoleIds.AGENT_VIEWER,
|
||||
viewInsights: true,
|
||||
},
|
||||
],
|
||||
updated: [],
|
||||
revoked: [],
|
||||
insightsChanges: [
|
||||
{ action: 'assigned', principal: { type: PrincipalType.ROLE, id: 'USER' } },
|
||||
],
|
||||
errors: [],
|
||||
});
|
||||
const req = createMockReq({
|
||||
params: { resourceType: ResourceType.AGENT, resourceId: agentObjectId },
|
||||
body: {
|
||||
updated: [
|
||||
{
|
||||
type: PrincipalType.ROLE,
|
||||
id: 'USER',
|
||||
accessRoleId: AccessRoleIds.AGENT_VIEWER,
|
||||
viewInsights: true,
|
||||
},
|
||||
],
|
||||
removed: [],
|
||||
},
|
||||
user: {
|
||||
_id: new mongoose.Types.ObjectId(),
|
||||
id: 'admin-id',
|
||||
name: 'Admin',
|
||||
role: SystemRoles.ADMIN,
|
||||
tenantId: 'tenant-a',
|
||||
},
|
||||
});
|
||||
const res = createMockRes();
|
||||
|
||||
await updateResourcePermissions(req, res);
|
||||
|
||||
expect(mockRecordAuditEntry).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
action: 'permission.insights_assigned',
|
||||
tenantId: 'tenant-a',
|
||||
target: expect.objectContaining({ id: 'agent-a' }),
|
||||
metadata: { principalType: PrincipalType.ROLE, principalId: 'USER' },
|
||||
}),
|
||||
{ failClosed: false },
|
||||
);
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(res.json.mock.calls[0][0].results.principals[0]).toEqual(
|
||||
expect.objectContaining({ viewInsights: true }),
|
||||
);
|
||||
});
|
||||
|
||||
it('restores unaudited Insights transitions when fail-closed auditing fails', async () => {
|
||||
process.env.AUDIT_LOG_FAIL_CLOSED = 'true';
|
||||
const changes = [{ action: 'assigned', principal: { type: PrincipalType.ROLE, id: 'USER' } }];
|
||||
mockBulkUpdateResourcePermissions.mockResolvedValue({
|
||||
granted: [],
|
||||
updated: [],
|
||||
revoked: [],
|
||||
insightsChanges: changes,
|
||||
errors: [],
|
||||
});
|
||||
mockRecordAuditEntry.mockRejectedValue(new Error('audit unavailable'));
|
||||
const req = createMockReq({
|
||||
params: { resourceType: ResourceType.AGENT, resourceId: agentObjectId },
|
||||
body: {
|
||||
updated: [
|
||||
{
|
||||
type: PrincipalType.ROLE,
|
||||
id: 'USER',
|
||||
accessRoleId: AccessRoleIds.AGENT_VIEWER,
|
||||
viewInsights: true,
|
||||
},
|
||||
],
|
||||
removed: [],
|
||||
},
|
||||
user: { id: 'admin-id', role: SystemRoles.ADMIN, tenantId: 'tenant-a' },
|
||||
});
|
||||
const res = createMockRes();
|
||||
|
||||
await updateResourcePermissions(req, res);
|
||||
|
||||
expect(mockRestoreInsightsPermissionChanges).toHaveBeenCalledWith({
|
||||
resourceType: ResourceType.AGENT,
|
||||
resourceId: agentObjectId,
|
||||
changes,
|
||||
});
|
||||
expect(res.status).toHaveBeenCalledWith(500);
|
||||
});
|
||||
|
||||
it('keeps a successful permission response when audit target lookup fails open', async () => {
|
||||
mockBulkUpdateResourcePermissions.mockResolvedValue({
|
||||
granted: [],
|
||||
updated: [],
|
||||
revoked: [],
|
||||
insightsChanges: [
|
||||
{ action: 'assigned', principal: { type: PrincipalType.ROLE, id: 'USER' } },
|
||||
],
|
||||
errors: [],
|
||||
});
|
||||
db.getAgent.mockRejectedValue(new Error('agent lookup unavailable'));
|
||||
const req = createMockReq({
|
||||
params: { resourceType: ResourceType.AGENT, resourceId: agentObjectId },
|
||||
user: { id: 'admin-id', role: SystemRoles.ADMIN, tenantId: 'tenant-a' },
|
||||
});
|
||||
const res = createMockRes();
|
||||
|
||||
await updateResourcePermissions(req, res);
|
||||
|
||||
expect(mockRestoreInsightsPermissionChanges).not.toHaveBeenCalled();
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
});
|
||||
|
||||
it('does not expose the protected bit in non-admin mutation responses', async () => {
|
||||
mockBulkUpdateResourcePermissions.mockResolvedValue({
|
||||
granted: [
|
||||
{
|
||||
type: PrincipalType.USER,
|
||||
id: revokedUserId,
|
||||
accessRoleId: AccessRoleIds.AGENT_EDITOR,
|
||||
viewInsights: true,
|
||||
},
|
||||
],
|
||||
updated: [],
|
||||
revoked: [],
|
||||
insightsChanges: [],
|
||||
errors: [],
|
||||
});
|
||||
const req = createMockReq({
|
||||
params: { resourceType: ResourceType.AGENT, resourceId: agentObjectId },
|
||||
body: {
|
||||
updated: [
|
||||
{
|
||||
type: PrincipalType.USER,
|
||||
id: revokedUserId,
|
||||
accessRoleId: AccessRoleIds.AGENT_EDITOR,
|
||||
},
|
||||
],
|
||||
removed: [],
|
||||
},
|
||||
});
|
||||
const res = createMockRes();
|
||||
|
||||
await updateResourcePermissions(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(res.json.mock.calls[0][0].results.principals[0]).not.toHaveProperty('viewInsights');
|
||||
});
|
||||
|
||||
it('removes agent from revoked users favorites on AGENT resource type', async () => {
|
||||
|
|
|
|||
|
|
@ -3610,9 +3610,11 @@ describe('ResumableAgentController resume metadata', () => {
|
|||
const req = createFailedRequest({
|
||||
conversationId: undefined,
|
||||
clientRequestId: 'failed-new-conversation',
|
||||
agent_id: 'unverified-agent',
|
||||
parentMessageId: '00000000-0000-0000-0000-000000000000',
|
||||
endpointOption: {
|
||||
endpoint: 'azureOpenAI',
|
||||
agent_id: 'unverified-agent',
|
||||
modelOptions: { model: 'gpt-4o' },
|
||||
chatProjectId: '507f1f77bcf86cd799439011',
|
||||
},
|
||||
|
|
@ -3643,7 +3645,7 @@ describe('ResumableAgentController resume metadata', () => {
|
|||
model: 'gpt-4o',
|
||||
chatProjectId: '507f1f77bcf86cd799439011',
|
||||
}),
|
||||
expect.any(Object),
|
||||
expect.objectContaining({ initialAgentId: null }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -272,6 +272,7 @@ async function saveErrorTurn(
|
|||
liveUserMessage,
|
||||
liveResponseMessageId,
|
||||
sender,
|
||||
initialAgentId,
|
||||
},
|
||||
) {
|
||||
try {
|
||||
|
|
@ -413,7 +414,15 @@ async function saveErrorTurn(
|
|||
await saveConvo(
|
||||
reqCtx,
|
||||
{ conversationId, ...convoFields },
|
||||
seedConvo ? { context } : { context, noUpsert: true },
|
||||
seedConvo
|
||||
? {
|
||||
context,
|
||||
initialAgentId:
|
||||
typeof initialAgentId === 'string' && !isEphemeralAgentId(initialAgentId)
|
||||
? initialAgentId
|
||||
: null,
|
||||
}
|
||||
: { context, noUpsert: true },
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error('[AgentController] Failed to persist error turn', err);
|
||||
|
|
@ -1371,6 +1380,7 @@ const ResumableAgentController = async (req, res, next, initializeClient, addTit
|
|||
});
|
||||
|
||||
let client = null;
|
||||
let verifiedInitialAgentId = null;
|
||||
let jobCreatedAt;
|
||||
let providerExecutionId;
|
||||
let releaseEventChildLease;
|
||||
|
|
@ -1763,6 +1773,12 @@ const ResumableAgentController = async (req, res, next, initializeClient, addTit
|
|||
});
|
||||
startupTelemetry?.mark('client_initialized');
|
||||
client = result.client;
|
||||
if (
|
||||
typeof client?.options?.agent?.id === 'string' &&
|
||||
!isEphemeralAgentId(client.options.agent.id)
|
||||
) {
|
||||
verifiedInitialAgentId = client.options.agent.id;
|
||||
}
|
||||
|
||||
/** Request-shape validation rejects every known edit/regenerate path, but
|
||||
* the client owns the final persistence decision. Fail closed if a future
|
||||
|
|
@ -2997,6 +3013,7 @@ const ResumableAgentController = async (req, res, next, initializeClient, addTit
|
|||
liveUserMessage: userMessage,
|
||||
liveResponseMessageId,
|
||||
sender: client?.sender,
|
||||
initialAgentId: verifiedInitialAgentId,
|
||||
}),
|
||||
})) === true;
|
||||
/** A true completion means this owner won the terminal CAS and
|
||||
|
|
@ -3201,6 +3218,7 @@ const ResumableAgentController = async (req, res, next, initializeClient, addTit
|
|||
endpointOption,
|
||||
isNewConvo,
|
||||
errorText: initializationError,
|
||||
initialAgentId: verifiedInitialAgentId,
|
||||
}),
|
||||
})
|
||||
: GenerationJobManager.completeJob(streamId, initializationError, jobCreatedAt);
|
||||
|
|
|
|||
|
|
@ -453,11 +453,14 @@ async function saveConversation(req, conversationId, agentId, agent) {
|
|||
{
|
||||
conversationId,
|
||||
endpoint: EModelEndpoint.agents,
|
||||
agentId,
|
||||
agent_id: agentId,
|
||||
...(title != null && { title }),
|
||||
model: agent?.model,
|
||||
},
|
||||
{ context: 'Responses API - save conversation' },
|
||||
{
|
||||
context: 'Responses API - save conversation',
|
||||
initialAgentId: agent?.id === agentId ? agentId : null,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue