CedarCopilot wants to merge 1 commit into staging from fix/chat-thread-cross-user-idor
Live on prod, no production signal yet
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.
Verification reached its 130s outer budget before the verifier returned. Libra is keeping this intent verifying until the next check.
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.
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.@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.pnpm --filter @zero/server run types , cleanpnpm --filter @zero/mail run types , cleanpnpm --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
This PR closes caller-supplied chat-thread ownership gaps and preserves administered-user scope when navigating from playbooks into related documents.
chat_thread:user authorization policy and routes cross-user chat reads through the shared effective-actor authority.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
| Surface | Requests | Errors | p95 | Users | Verdict |
|---|---|---|---|---|---|
| db.getConnectionRecord | 2799 ā 4210 | 7 ā 1 (0.0%) | 100 ms ā 101 ms | 0 | Working 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.syncThread | 13 ā 10 | 7 ā 1 (10%) | 348 ms ā 804 ms | 0 | Insufficient traffic 10 requests, under the 20 Libra needs |
| chat.createThread | no baseline ā 0 | no baseline ā 0 (0%) | not measured | not counted | Unavailable ClickHouse reads are unavailable, so Libra could not measure this surface. |