Skip to content

🛍️ fix: Discover Managed Agents in Marketplace Search - #16324

Merged
danny-avila merged 5 commits into
devfrom
lia/manage-agents-discovery
Sep 25, 2026
Merged

danny-avila merged 5 commits into
devfrom
lia/manage-agents-discovery

Conversation

@lia-by-librechat

@lia-by-librechat lia-by-librechat Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Summary

A user with the manage:agents system capability can open an unshared agent directly, but the browser-facing GET /api/agents list and marketplace search omit it because the handler filters only by explicit agent ACL entries. A manager who does not know the agent ID cannot discover it.

The browser list now honors the same management capability. Managers can find agents within their authenticated tenant, or only legacy untenanted agents when their request has no tenant; non-managers continue to see only ACL-accessible agents. Search, category, promoted, pagination, public status and VIEW skill-scope filtering are unchanged. Capability-check failures fall back to ACL filtering.

Fixes #16317

How it works

GET /api/agents
  getAgentListAccess: start VIEW/EDIT ACL reads alongside the capability check
    manage:agents -> ignore ACL results; accessibleIds: null; all page matches editable
    otherwise     -> plain string ACL IDs; preserve VIEW/EDIT filtering
  getListAgentsByAccess:
    null           -> explicitly filter by req.user.tenantId (null -> legacy only)
    [] or omitted  -> no matches
  avatar refresh:
    manager        -> presign only S3 avatars on the returned page, cache up to 1000 URLs for at most the original 30-minute window, keyed by user and tenant and guarded by the stored avatar path; never write updatedAt
    ordinary user  -> preserve existing pre-query ACL-scoped refresh and cursor ordering

The browser controller wires the typed access and avatar helpers in packages/api to the tenant-filtered query in packages/data-schemas. The capability check is shared with the machine-authenticated management list. In the non-manager startup path, capability and ACL reads overlap instead of adding a serial round trip.

Type of change

  • Bug fix
  • Tests / tooling / CI

Testing

Tested environments/configuration: MongoDB 8.2.1 in memory with an actual role-scoped manage:agents grant; authenticated tenant IDs and legacy requests without a tenant; S3-avatar list pagination. Managers can discover unshared agents by search without crossing tenant boundaries.

Automated tests:

  • api/server/controllers/agents/v1.spec.js and api/server/controllers/avatar.contentFilter.spec.js: 49 passed across list security, S3 avatar refresh and upload content-filter coverage, including tenant-aware list cache access and successful upload invalidation.
  • packages/data-schemas/src/methods/agent.spec.ts access-list security: 20 passed, including explicit tenant/legacy separation without ambient tenant context and ACL string-ID casting.
  • packages/api/src/agents/reads.spec.ts and listingAvatars.spec.ts: 24 passed, including parallel ACL/capability reads, bounded manager avatar refresh, cache expiry, shared cache keys and cross-user avatar replacement.
  • tsc --noEmit in packages/api and packages/data-schemas: passed.
  • npm run build --workspace @librechat/data-schemas and npm run build --workspace @librechat/api: passed.
  • npm run static-checks on the staged diff: passed.

Screenshots / recordings

No layout or styling changes; this changes which existing marketplace cards appear. No before/after screenshot was captured because local Chromium cannot start without the sandbox's missing libatk system library. The running-list behavior is covered by Mongo-backed controller tests; CI runs the Lighthouse browser lane.

Risk / compatibility

An unrestricted ID filter is only used after a successful manage:agents check. Even without request tenant context, the query explicitly restricts to the authenticated tenant, or to legacy records where tenantId is null/missing. Empty/omitted ACL lists still match nothing, and invalid permission masks reject before any bypass. Ordinary users' ACL reads are issued in parallel with the capability check; managers incur speculative ACL reads but never depend on their outcome. For managers, presigned S3 URLs are cached per user and tenant, bounded at the existing 1000-agent budget and expiring no later than the first signed page; they are not persisted, avoiding full-tenant avatar work and cursor mutation. Page-only cache entries cannot suppress the old full ACL refresh after capability downgrade. Avatar uploads invalidate the same user/tenant key that listing reads; if another user uploads, the cached URL is re-signed when the stored avatar path changes. Ordinary users keep their existing pre-query avatar refresh.

Checklist

  • I reviewed my own changes
  • Relevant tests have been added or updated
  • Existing relevant tests pass
  • The change does not introduce new warnings or errors
  • User-facing or complex behavior is documented where necessary
  • Required dependency changes have been merged/published
  • Required documentation PR: N/A

@lia-by-librechat

Copy link
Copy Markdown
Contributor Author

Review handoff for head f64c7931b4dc4c0d45ee7298ee60ea784d8fbea8: browser marketplace agent lists now honor the existing manage:agents bypass, keep ACL-denied defaults, and retain tenant-scoped pagination and VIEW skill sanitization. Focused Mongo-backed list tests, both changed TypeScript typechecks, workspace builds and staged static checks passed.

@lia-by-librechat

Copy link
Copy Markdown
Contributor Author

Review handoff for head 23bc3b246cd546b0c08258403ed1a471a2f404db: agent managers can now discover tenant-scoped agents in marketplace search without ACL ID scans; invalid permission masks still fail before the bypass. The browser-list tests, API authorization tests, both TypeScript typechecks, workspace build and scoped static checks passed.

@danny-avila

Copy link
Copy Markdown
Collaborator

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-25T02:48:47.372768Z af339f1 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 23bc3b246c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/api/src/agents/listing.ts Outdated
if (typeof requiredPermissions !== 'number' || requiredPermissions < 1) {
throw new Error('requiredPermissions must be a positive number');
}
if (await hasManageAgentsCapability(user, deps)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Start ACL reads before awaiting the capability lookup

For every authenticated non-manager load, this await finishes the database-backed capability check before the two agent ACL lookups at lines 25–30 can even start. This endpoint is on the initial client path (Root.tsx mounts useAgentsMap, which requests the VIEW list), so the change adds a serialized database round trip to ordinary startup and marketplace loads; start the capability and scoped ACL reads together, then discard the ACL result when the capability succeeds.

AGENTS.md reference: AGENTS.md:L49-L54

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 174195f1a392bfe4170141b941ae1971938283d3: capability and both ACL reads now start concurrently. The ACL results are ignored for managers, and their errors cannot block a successful capability bypass.

Comment thread packages/api/src/agents/listing.ts Outdated
user: IUser,
requiredPermissions: PermissionBits,
deps: Pick<AgentManagementReadDeps, 'hasCapability' | 'findAccessibleResources'>,
): Promise<{ accessibleIds: Types.ObjectId[] | null; editableIds: Types.ObjectId[] | null }> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep Mongoose IDs out of the exported resolver

This newly exported packages/api function exposes Types.ObjectId in its return contract, making every consumer and substitute implementation depend on Mongoose even though this layer only needs plain agent identifiers. Define the exported dependency/result contract with plain IDs and leave ObjectId conversion behind the data-schemas method instead of widening the existing storage-engine leak.

AGENTS.md reference: AGENTS.md:L70-L75

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 174195f1a392bfe4170141b941ae1971938283d3: the exported resolver now returns plain string IDs and has no Mongoose type in its public signature; data-schemas accepts those IDs in its existing filtered query.

@@ -1456,7 +1457,7 @@ export function createAgentMethods(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Retain tenant scoping when bypassing the ID filter

When a manage:agents capability holder has no tenantId and TENANT_ISOLATION_STRICT is left at its documented default of false, tenantContextMiddleware deliberately continues without a tenant scope (packages/api/src/middleware/tenant.ts:162-168) and the Mongoose plugin consequently injects no predicate. Removing _id here then turns Agent.find(baseQuery) into a cross-tenant query, exposing—and during avatar refresh potentially updating—agents belonging to every tenant; the unrestricted path must add an explicit caller tenant/legacy-tenant predicate or reject the bypass when no tenant can be established.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 174195f1a392bfe4170141b941ae1971938283d3: unrestricted list queries always add an explicit authenticated tenant predicate; missing tenant context is restricted to legacy tenantId null/missing records. Covered by Mongo-backed tests without ambient request context.

Comment thread packages/api/src/agents/listing.ts Outdated
Comment on lines +15 to +16
if (await hasManageAgentsCapability(user, deps)) {
return { accessibleIds: null, editableIds: null };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid full-tenant avatar refreshes for managers

For a manager with a cold per-user avatar cache, this resolver returns accessibleIds: null, which is then reused by resolveAvatarRefresh before the filtered list query. A search for one agent therefore selects up to MAX_AVATAR_REFRESH_AGENTS (1000) S3-backed agents from the entire tenant and blocks the response while refreshListAvatars performs an S3 refresh plus an updateAgent write for each changed URL; large tenants can turn the marketplace search into hundreds of unrelated network calls and database writes every 30 minutes for each manager. Keep the unrestricted scope out of this background refresh, or make the refresh tenant-shared and independent of the search response.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 174195f1a392bfe4170141b941ae1971938283d3: managers skip the full-tenant prequery and only presign S3 avatars in the returned page. No agent writes occur after the cursor snapshot; URLs are cached per user and tenant. Paginated browser test proves later pages are not lost.

@lia-by-librechat

Copy link
Copy Markdown
Contributor Author

Review handoff for exact pushed head 174195f1a392bfe4170141b941ae1971938283d3: resolved all four inline findings (parallel ACL/capability reads, plain ID boundary, explicit tenant/legacy predicate, visible-page manager S3 refresh). Focused browser, Mongo list and typed API tests, both workspace typechecks, package builds and staged static checks passed; CI for this head is running.

@lia-by-librechat

Copy link
Copy Markdown
Contributor Author

Review handoff for exact pushed head e485d1d179937c45ab5f909edad0ac2f22dcdc51: all four original P1 findings are addressed. A second self-review also bounded the manager page-avatar cache to 1000 IDs and its original 30-minute signing window, separated page-only from full ACL caches on a capability downgrade, and removed a redundant editable-ID remap. Focused browser (44), API helper (22), and data-schemas list (20) tests, TypeScript workspace checks, package builds and scoped static checks passed. CI for this head is running.

@danny-avila

Copy link
Copy Markdown
Collaborator

@codex review the latest head

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e485d1d179

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread api/server/controllers/agents/v1.js Outdated

const cache = getLogStores(CacheKeys.S3_EXPIRY_INTERVAL);
const refreshKey = `${userId}:agents_avatar_refresh`;
const refreshKey = `${userId}:${req.user.tenantId ?? ''}:agents_avatar_refresh`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Invalidate the tenant-scoped avatar cache after upload

When a user has already populated this cache and then replaces an S3 agent avatar, the upload handler still deletes the old key ${req.user.id}:agents_avatar_refresh at line 2014 rather than this new tenant-scoped key. Because the upload also deletes the old avatar object, subsequent list responses can keep substituting its cached signed URL for up to 30 minutes, leaving the newly uploaded avatar broken or stale; update the invalidation path to construct the same tenant-aware key.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in af339f1eb2de23ae90b4f4973cc0d4e8633b7179: both the list writer and upload invalidator call one typed cache-key builder with the authenticated user and tenant. A successful upload regression checks the deleted tenant key, and the list test checks its matching read key. Manager page caches also compare the stored avatar path before reusing a URL, so a different user's upload cannot leave a stale signed URL.

@lia-by-librechat

Copy link
Copy Markdown
Contributor Author

Review handoff for exact pushed head af339f1eb2de23ae90b4f4973cc0d4e8633b7179: resolved the new avatar-upload invalidation finding with a shared tenant-qualified cache key and a successful upload regression. A manager's cache also re-signs an avatar replaced by another user rather than serving a stale URL. Focused browser/upload tests (49), typed helper tests (24), API typecheck and package build, and staged static checks pass. CI for this head is running.

@danny-avila

Copy link
Copy Markdown
Collaborator

@codex review the latest head

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Already looking forward to the next diff.

Reviewed commit: af339f1eb2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@danny-avila
danny-avila merged commit 5862b3b into dev Sep 25, 2026
28 checks passed
@danny-avila
danny-avila deleted the lia/manage-agents-discovery branch September 25, 2026 02:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Marketplace/list endpoint for agents ignores the manage:agents capability bypass that direct-fetch honors

2 participants