From 73227f3da7b039a400350fa2d9aaa88a38a5a7a7 Mon Sep 17 00:00:00 2001 From: Sam Xu Date: Mon, 4 May 2026 00:43:52 -0700 Subject: [PATCH] fix(pods): default GET /api/pods to membership-filtered listing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previous behavior: GET /api/pods returned every chat/team/study/games pod on the instance to every authenticated user. Only the personal pod types (agent-admin / agent-room / agent-dm) had membership gating. This made the V2 sidebar unusable on a shared / multi-tenant instance — a fresh user account would see dozens of unrelated pods they have no business browsing. Especially bad for demos and test accounts on the dev cluster, which now sees ~58 pods leak. This commit extends the existing membership filter (used for the three personal pod types) to the default listing. Behavior: GET /api/pods — returns only pods the user is a member of GET /api/pods?scope=all — returns every readable pod (legacy) GET /api/pods?type=chat — returns only chat pods the user is in GET /api/pods?type=agent-dm — unchanged (already membership-gated) Global admins still bypass — preserves the audit surface for moderating the 1:1 invariant on agent-rooms (ADR-001 §3.10). Net diff: 11 lines in podController.getAllPods. No frontend changes — V2 sidebar's existing call hits the default path and will Just Work after deploy. For surfaces that legitimately need to enumerate all pods (admin tools, future marketplace browse, pod-discovery), pass `?scope=all` explicitly. That path still requires the user have read permission via the existing pod ACL. Co-Authored-By: Claude Opus 4.7 (1M context) --- backend/controllers/podController.ts | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/backend/controllers/podController.ts b/backend/controllers/podController.ts index 1aed57d0..fad673c5 100644 --- a/backend/controllers/podController.ts +++ b/backend/controllers/podController.ts @@ -162,11 +162,27 @@ exports.getAllPods = async (req: any, res: any) => { .populate('parentPod', 'name _id') .sort({ updatedAt: -1 }); - // Personal pod types: only return pods the requester belongs to. - // Global admins bypass the membership filter so they can audit every - // agent-room / agent-admin / agent-dm in the instance — the moderation - // surface for the 1:1 invariant on agent-rooms (ADR-001 §3.10). - if ((type === 'agent-admin' || type === 'agent-room' || type === 'agent-dm') && req.userId) { + // Membership filter — return only pods the requester belongs to. + // + // Personal pod types (agent-admin / agent-room / agent-dm) have always + // been membership-gated. As of this commit, the SAME rule extends to + // chat/team/study/games pods on the default listing (no `type` query + // param). The previous behavior leaked every chat pod on the instance + // to every authenticated user, which made the V2 sidebar unusable on + // a multi-tenant or shared dev instance and broke isolation for + // demos / test accounts. + // + // To opt back into the legacy "list everything I have access to read" + // behavior — useful for admin tooling, marketplace browsing, and + // the pod-discovery surface — pass `?scope=all`. That path still + // requires admin or explicit join-policy. + // + // Global admins bypass membership filtering so they can audit every + // pod in the instance — same moderation surface as before for the + // 1:1 invariant on agent-rooms (ADR-001 §3.10). + const scope = String(req.query?.scope || 'mine').toLowerCase(); + const isPersonal = type === 'agent-admin' || type === 'agent-room' || type === 'agent-dm'; + if (req.userId && (isPersonal || scope === 'mine')) { const isAdmin = await isGlobalAdminRequest(req); if (!isAdmin) { const uid = String(req.userId);