diff --git a/.vscode/launch.json b/.vscode/launch.json index e393568b16..be41a3836d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,7 +8,8 @@ "skipFiles": ["/**"], "program": "${workspaceFolder}/api/server/index.js", "env": { - "NODE_ENV": "production" + "NODE_ENV": "production", + "NODE_TLS_REJECT_UNAUTHORIZED": "0" }, "console": "integratedTerminal", "envFile": "${workspaceFolder}/.env" diff --git a/api/server/controllers/agents/v1.js b/api/server/controllers/agents/v1.js index 483321092f..75b2278702 100644 --- a/api/server/controllers/agents/v1.js +++ b/api/server/controllers/agents/v1.js @@ -15,7 +15,12 @@ const { deleteAgent, getListAgentsByAccess, } = require('~/models/Agent'); -const { grantPermission, findAccessibleResources } = require('~/server/services/PermissionService'); +const { + grantPermission, + findAccessibleResources, + findPubliclyAccessibleResources, + hasPublicPermission, +} = require('~/server/services/PermissionService'); const { getStrategyFunctions } = require('~/server/services/Files/strategies'); const { updateAgentProjects, revertAgentVersion } = require('~/models/Agent'); const { resizeAvatar } = require('~/server/services/Files/images/avatar'); @@ -134,6 +139,14 @@ const getAgentHandler = async (req, res, expandProperties = false) => { // @deprecated - isCollaborative replaced by ACL permissions agent.isCollaborative = !!agent.isCollaborative; + // Check if agent is public + const isPublic = await hasPublicPermission({ + resourceType: 'agent', + resourceId: agent._id, + requiredPermissions: PermissionBits.VIEW, + }); + agent.isPublic = isPublic; + if (agent.author !== author) { delete agent.author; } @@ -152,6 +165,7 @@ const getAgentHandler = async (req, res, expandProperties = false) => { projectIds: agent.projectIds, // @deprecated - isCollaborative replaced by ACL permissions isCollaborative: agent.isCollaborative, + isPublic: agent.isPublic, version: agent.version, // Safe metadata createdAt: agent.createdAt, @@ -392,14 +406,23 @@ const getListAgentsHandler = async (req, res) => { resourceType: 'agent', requiredPermissions: PermissionBits.VIEW, }); - + const publiclyAccessibleIds = await findPubliclyAccessibleResources({ + resourceType: 'agent', + requiredPermissions: PermissionBits.VIEW, + }); // Use the new ACL-aware function const data = await getListAgentsByAccess({ accessibleIds, otherParams: {}, // Can add query params here if needed - }); - + if (data?.data?.length) { + data.data = data.data.map((agent) => { + if (publiclyAccessibleIds.some(id => id.equals(agent._id))) { + agent.isPublic = true; + } + return agent; + }); + } return res.json(data); } catch (error) { logger.error('[/Agents] Error listing Agents', error); diff --git a/api/server/services/PermissionService.js b/api/server/services/PermissionService.js index 4b475ab300..59cfb4159a 100644 --- a/api/server/services/PermissionService.js +++ b/api/server/services/PermissionService.js @@ -17,6 +17,7 @@ const { findAccessibleResources: findAccessibleResourcesACL, hasPermission, getEffectivePermissions: getEffectivePermissionsACL, + findEntriesByPrincipalsAndResource, } = require('~/models'); const { AclEntry, AccessRole, Group } = require('~/db/models'); @@ -178,6 +179,37 @@ const findAccessibleResources = async ({ userId, resourceType, requiredPermissio } }; +/** + * Find all publicly accessible resources of a specific type + * @param {Object} params - Parameters for finding publicly accessible resources + * @param {string} params.resourceType - Type of resource (e.g., 'agent') + * @param {number} params.requiredPermissions - The minimum permission bits required (e.g., 1 for VIEW, 3 for VIEW+EDIT) + * @returns {Promise} Array of resource IDs + */ +const findPubliclyAccessibleResources = async ({ resourceType, requiredPermissions }) => { + try { + if (typeof requiredPermissions !== 'number' || requiredPermissions < 1) { + throw new Error('requiredPermissions must be a positive number'); + } + + // Find all public ACL entries where the public principal has at least the required permission bits + const entries = await AclEntry.find({ + principalType: 'public', + resourceType, + permBits: { $bitsAllSet: requiredPermissions }, + }).distinct('resourceId'); + + return entries; + } catch (error) { + logger.error(`[PermissionService.findPubliclyAccessibleResources] Error: ${error.message}`); + // Re-throw validation errors + if (error.message.includes('requiredPermissions must be')) { + throw error; + } + return []; + } +}; + /** * Get available roles for a resource type * @param {Object} params - Parameters for getting available roles @@ -407,6 +439,41 @@ const syncUserEntraGroupMemberships = async (user, accessToken, session = null) } }; +/** + * Check if public has a specific permission on a resource + * @param {Object} params - Parameters for checking public permission + * @param {string} params.resourceType - Type of resource (e.g., 'agent') + * @param {string|mongoose.Types.ObjectId} params.resourceId - The ID of the resource + * @param {number} params.requiredPermissions - The permission bits required (e.g., 1 for VIEW, 3 for VIEW+EDIT) + * @returns {Promise} Whether public has the required permission bits + */ +const hasPublicPermission = async ({ resourceType, resourceId, requiredPermissions }) => { + try { + if (typeof requiredPermissions !== 'number' || requiredPermissions < 1) { + throw new Error('requiredPermissions must be a positive number'); + } + + // Use public principal to check permissions + const publicPrincipal = [{ principalType: 'public' }]; + + const entries = await findEntriesByPrincipalsAndResource( + publicPrincipal, + resourceType, + resourceId, + ); + + // Check if any entry has the required permission bits + return entries.some(entry => (entry.permBits & requiredPermissions) === requiredPermissions); + } catch (error) { + logger.error(`[PermissionService.hasPublicPermission] Error: ${error.message}`); + // Re-throw validation errors + if (error.message.includes('requiredPermissions must be')) { + throw error; + } + return false; + } +}; + /** * Bulk update permissions for a resource (grant, update, revoke) * Efficiently handles multiple permission changes in a single transaction @@ -615,6 +682,8 @@ module.exports = { checkPermission, getEffectivePermissions, findAccessibleResources, + findPubliclyAccessibleResources, + hasPublicPermission, getAvailableRoles, bulkUpdateResourcePermissions, ensurePrincipalExists, diff --git a/client/src/components/SidePanel/Agents/AgentSelect.tsx b/client/src/components/SidePanel/Agents/AgentSelect.tsx index 496dd4ee6d..0c4ab8aced 100644 --- a/client/src/components/SidePanel/Agents/AgentSelect.tsx +++ b/client/src/components/SidePanel/Agents/AgentSelect.tsx @@ -43,9 +43,7 @@ export default function AgentSelect({ const resetAgentForm = useCallback( (fullAgent: Agent) => { - const { instanceProjectId } = startupConfig ?? {}; - const isGlobal = - (instanceProjectId != null && fullAgent.projectIds?.includes(instanceProjectId)) ?? false; + const isGlobal = fullAgent.isPublic ?? false; const update = { ...fullAgent, provider: createProviderOption(fullAgent.provider), diff --git a/client/src/hooks/Endpoint/useEndpoints.ts b/client/src/hooks/Endpoint/useEndpoints.ts index 63962fd002..5a074a0fc6 100644 --- a/client/src/hooks/Endpoint/useEndpoints.ts +++ b/client/src/hooks/Endpoint/useEndpoints.ts @@ -125,8 +125,7 @@ export const useEndpoints = ({ if (ep === EModelEndpoint.agents && agents.length > 0) { result.models = agents.map((agent) => ({ name: agent.id, - isGlobal: - (instanceProjectId != null && agent.projectIds?.includes(instanceProjectId)) ?? false, + isGlobal: agent.isPublic ?? false, })); result.agentNames = agents.reduce((acc, agent) => { acc[agent.id] = agent.name || ''; diff --git a/client/src/utils/forms.tsx b/client/src/utils/forms.tsx index fc1ce0c078..d8fa4cc8a8 100644 --- a/client/src/utils/forms.tsx +++ b/client/src/utils/forms.tsx @@ -63,8 +63,7 @@ export const processAgentOption = ({ fileMap?: Record; instanceProjectId?: string; }): TAgentOption => { - const isGlobal = - (instanceProjectId != null && _agent?.projectIds?.includes(instanceProjectId)) ?? false; + const isGlobal = _agent?.isPublic ?? false; const agent: TAgentOption = { ...(_agent ?? ({} as Agent)), label: _agent?.name ?? '', diff --git a/packages/data-provider/src/types/assistants.ts b/packages/data-provider/src/types/assistants.ts index df2c926bb9..ce33b65450 100644 --- a/packages/data-provider/src/types/assistants.ts +++ b/packages/data-provider/src/types/assistants.ts @@ -226,6 +226,7 @@ export type Agent = { hide_sequential_outputs?: boolean; artifacts?: ArtifactModes; recursion_limit?: number; + isPublic?: boolean; version?: number; };