mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-10-23 10:36:09 +00:00
🔒 fix: enforce REMOTE_AGENT ACL on handoff sub-agents for API routes
Addresses the second P1 codex finding on #12740: the OpenAI-compat `/v1/chat/completions` and Open Responses `/v1/responses` routes gate the primary agent on `REMOTE_AGENT` (via `createCheckRemoteAgentAccess`), but `discoverConnectedAgents` was checking handoff sub-agents against the looser in-app `AGENT` resource type. That allowed a remote caller who could reach the orchestrator but had only in-app visibility on a sub-agent to invoke it via the API — bypassing the remote-sharing boundary. Adds an optional `resourceType` param to `discoverConnectedAgents` (defaulting to `AGENT` for the chat UI path) and passes `ResourceType.REMOTE_AGENT` from both API controllers so every discovered sub-agent clears the same sharing boundary enforced at route entry.
This commit is contained in:
parent
7418dd83b5
commit
e491a23a77
4 changed files with 46 additions and 1 deletions
|
|
@ -278,6 +278,10 @@ const OpenAIChatCompletionController = async (req, res) => {
|
|||
requestFiles: [],
|
||||
conversationId,
|
||||
parentMessageId,
|
||||
// The route enforces REMOTE_AGENT on the primary; every discovered
|
||||
// sub-agent must clear the same sharing boundary, not the looser
|
||||
// in-app AGENT one.
|
||||
resourceType: ResourceType.REMOTE_AGENT,
|
||||
},
|
||||
{
|
||||
getAgent: db.getAgent,
|
||||
|
|
|
|||
|
|
@ -416,6 +416,10 @@ const createResponse = async (req, res) => {
|
|||
requestFiles: [],
|
||||
conversationId,
|
||||
parentMessageId,
|
||||
// The route enforces REMOTE_AGENT on the primary; every discovered
|
||||
// sub-agent must clear the same sharing boundary, not the looser
|
||||
// in-app AGENT one.
|
||||
resourceType: ResourceType.REMOTE_AGENT,
|
||||
},
|
||||
{
|
||||
getAgent: db.getAgent,
|
||||
|
|
|
|||
|
|
@ -202,6 +202,34 @@ describe('discoverConnectedAgents', () => {
|
|||
expect(result.edges.map((e) => e.to)).toEqual(['B']);
|
||||
});
|
||||
|
||||
it('passes the configured resourceType (e.g. REMOTE_AGENT) to checkPermission', async () => {
|
||||
const primaryConfig = makeConfig('A', [{ from: 'A', to: 'B', edgeType: 'handoff' }]);
|
||||
const getAgent = jest.fn(async () => makeAgent('B', []));
|
||||
const checkPermission = jest.fn().mockResolvedValue(true);
|
||||
|
||||
await discoverConnectedAgents(
|
||||
{
|
||||
req: makeReq(),
|
||||
res: makeRes(),
|
||||
primaryConfig,
|
||||
allowedProviders: new Set(),
|
||||
modelsConfig: { openai: ['gpt-4o'] },
|
||||
loadTools: jest.fn(),
|
||||
resourceType: 'REMOTE_AGENT',
|
||||
},
|
||||
{
|
||||
getAgent,
|
||||
checkPermission,
|
||||
logViolation: jest.fn(),
|
||||
db: {} as never,
|
||||
},
|
||||
);
|
||||
|
||||
expect(checkPermission).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ resourceType: 'REMOTE_AGENT' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('drops edges whose `from` references a skipped agent (bidirectional graph)', async () => {
|
||||
// Orchestrator A with bidirectional handoffs A<->B. B is inaccessible,
|
||||
// so `A -> B` and `B -> A` must both be filtered — otherwise `B -> A`
|
||||
|
|
|
|||
|
|
@ -54,6 +54,14 @@ export interface DiscoverConnectedAgentsParams {
|
|||
requestFiles?: InitializeAgentParams['requestFiles'];
|
||||
conversationId?: string | null;
|
||||
parentMessageId?: string | null;
|
||||
/**
|
||||
* ResourceType to check each sub-agent's access against. Defaults to
|
||||
* `AGENT` for the in-app chat flow. Callers whose entry-point gates on
|
||||
* a different resource type (e.g. the OpenAI-compat controllers gate on
|
||||
* `REMOTE_AGENT`) must pass the matching resource type so sub-agents
|
||||
* don't bypass the same sharing boundary enforced at the route.
|
||||
*/
|
||||
resourceType?: string;
|
||||
}
|
||||
|
||||
export interface DiscoverConnectedAgentsDeps {
|
||||
|
|
@ -117,6 +125,7 @@ export async function discoverConnectedAgents(
|
|||
requestFiles,
|
||||
conversationId,
|
||||
parentMessageId,
|
||||
resourceType = ResourceType.AGENT,
|
||||
} = params;
|
||||
|
||||
const {
|
||||
|
|
@ -162,7 +171,7 @@ export async function discoverConnectedAgents(
|
|||
const hasAccess = await checkPermission({
|
||||
userId,
|
||||
role: req.user?.role,
|
||||
resourceType: ResourceType.AGENT,
|
||||
resourceType,
|
||||
resourceId: agent._id,
|
||||
requiredPermission: PermissionBits.VIEW,
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue