Libra CodeHub

CedarCopilot/cedar-mail

Branch: staging

fix(auth): close chat-thread cross-user IDOR, restore admin scope on Brain playbook nav

merged#2766CedarCopilot

CedarCopilot wants to merge 1 commit into staging from fix/chat-thread-cross-user-idor

Live on prod, no production signal yetTimeline and evidence
  1. Opened
    Sep 12, 2026, 11:36 AM
  2. Merged
    Sep 12, 2026, 11:54 AM
  3. Live on prod
    Sep 12, 2026, 11:54 AM
  4. Observed 3 days, 3 surfaces, 4,220 requests
    Sep 12, 2026, 11:54 AM
  5. Pipelines steady after this deploy
    Sep 12, 2026, 11:54 AM
  6. Sep 12, 2026, 11:54 AM
  7. Unobserved

    Live on prod, no production signal yet

    Sep 15, 2026, 11:48 AM

Behaviors Libra is checking

Chat thread reads now enforce self-or-same-organization-admin authorization, allowing an org admin to read a teammate's threads and messages while denying non-admins and admins from other organizations instead of exposing data or silently returning empty results.Degradedlow confidence

Strict CloudWatch fallback found 2 prod failure log lines for [run-chat-agent-sdk] query() failed (Claude Code process aborted by user) in the last 24h while checking getthreads, getmessages, listthreadsforagent, listthreadsforconversation, forbidden.

prod, checked Sep 15, 2026, 11:29 AM
Brain playbook navigation preserves the viewed targetUserId when opening the chat-agent playbook, opening or creating playbook documents, and opening newly created subagents so an admin's teammate view is not reset to the signed-in admin.Inconclusivelow confidence

Verification reached its 130s outer budget before the verifier returned. Libra is keeping this intent verifying until the next check.

prod, checked Sep 14, 2026, 4:45 AM
Chat thread creation now rejects a caller that supplies another user's userId, while continuing to allow creation under the caller's own account.Not checked
prod, checked Sep 15, 2026, 11:48 AM

Libra measured 1 of 3 production surfaces on prod. 1 surface had under 20 requests, so Libra has not judged it. Libra has 1 low-confidence degraded verdict it is still confirming. Libra checks hourly for 3 days after each deploy.

Summary

Two related admin-view scoping bugs, both about a caller-named userId/targetUserId losing its authorization check or getting dropped in transit:

  • chat.ts (IDOR , was unauthenticated cross-user read/write): getThreads, getMessages, listThreadsForAgent, listThreadsForConversation, and createThread accepted a client-supplied userId/targetUserId with no authorization check at all. Any authenticated caller could read (or, via createThread, forge) another user's chat threads and messages by naming their id , admin or not, same org or not. Every cross-user read now goes through authorize() via a new chat_thread:user policy row (SELF_OR_ORG_ADMIN), the same authority agent.* and documents.getDoc already use; createThread now refuses any foreign userId outright (read-only admin delegation, no admin write path). During review I deduplicated the four near-identical inline authorization blocks onto the existing assertActor helper, which already implements the exact self-path-short-circuit-then-authorize shape.
  • Brain playbook document nav (admin scope silently dropped): an org admin clicking an @resources/... reference or a subagent chip inside a teammate's open playbook document lost the admin's targetUserId on navigation , brainDocumentPath(doc.id) was called without it, even though the query that had just fetched that doc already carried it. The Knowledge explorer then re-fetched by bare documentId with no scope and hit the per-user ownership check as the admin instead of the teammate, so the admin got a permission failure browsing a teammate's own AOP resources/subagents , the exact bug reported. Fixed by threading targetUserId/ownerUserId through all four affected navigate(brainDocumentPath(...)) call sites, matching the pattern the tree view (PlaybookAopSection) already used correctly. Exhaustively verified every other brainDocumentPath call site in apps/mail; the two that remain unscoped (useOpenContextArtifact, BrainHero's recent-files rail) are legitimately self-only.

Test plan

  • pnpm --filter @zero/server run types , clean
  • pnpm --filter @zero/mail run types , clean
  • pnpm --filter @zero/server exec vitest run src/trpc/routes/__tests__/chat-router-org-admin.test.ts , 12/12 pass (admin can read a teammate's threads/messages; a plain member and an out-of-org admin are refused; the ordinary self path is untouched)
  • pnpm --filter @zero/server exec vitest run src/trpc/routes/__tests__/chat-agent-binding.test.ts , 10/10 pass (unaffected sibling suite)
  • pnpm --filter @zero/server exec eslint src/trpc/routes/chat.ts , clean

šŸ¤– Generated with Claude Code

https://claude.ai/code/session_0187itFj1uakga2xd7kPk4qp

Greptile Summary

This PR closes caller-supplied chat-thread ownership gaps and preserves administered-user scope when navigating from playbooks into related documents.

  • Adds a chat_thread:user authorization policy and routes cross-user chat reads through the shared effective-actor authority.
  • Rejects creation of chat threads attributed to another user.
  • Carries teammate scope through playbook resource and subagent document navigation.
  • Adds authorization-focused tests, although their database mock does not verify effective-user query scoping and the test contains casts prohibited by repository guidance.

Confidence Score: 4/5

The behavioral changes appear sound, but the repository's explicit no-casts requirement must be satisfied before merging; strengthening the security regression test's query-scope assertions is also recommended.

The authorization and navigation implementations preserve the intended effective user and fail closed across organizations. The remaining findings concern a security-focused mock that cannot detect ownership-predicate regressions and prohibited TypeScript assertions in the new test.

**F

Show production surfaces and changed-file mapping

Production surfaces

SurfaceRequestsErrorsp95UsersVerdict
db.getConnectionRecord2799 → 42107 → 1 (0.0%)100 ms → 101 ms0Working
4,210 requests since the deploy with 1 errors (7 errors in the 2,799-request baseline before it). p95 101 ms, was 100 ms.
aws-primary.syncThread13 → 107 → 1 (10%)348 ms → 804 ms0Insufficient traffic
10 requests, under the 20 Libra needs
chat.createThreadno baseline → 0no baseline → 0 (0%)not measurednot countedUnavailable
ClickHouse reads are unavailable, so Libra could not measure this surface.

Changed files → surfaces

  • apps/server/src/services/auth/authorize.tsdb.getConnectionRecordaws-primary.syncThread
  • apps/mail/app/(routes)/brain/playbooks/page.tsxno production surface mapped
  • apps/mail/modules/documents/playbook/CompositePlaybookDocument.tsxno production surface mapped
  • apps/mail/modules/documents/playbook/PlaybookDocument.tsxno production surface mapped
  • apps/mail/modules/documents/playbook/useSubagentCreation.tsxno production surface mapped
  • apps/server/src/trpc/routes/__tests__/chat-router-org-admin.test.tsno production surface mapped
  • apps/server/src/trpc/routes/chat.tsno production surface mapped