CedarCopilot wants to merge 1 commit into staging from fix/live-mail-tool-scope-and-bcc
Live on prod, no production signal yet
Libra has verdicts on 0 of 4 tracked behaviors on prod; 4 are still being checked. Libra checks hourly for 3 days after each deploy.
Three review findings on the live-mail grant shipped in #2649. All three were valid.
run-post-event-executor resolves the drafter's canonical agentId, stamps it on the sub-execution row it creates, and then built the executor's RequestContext without it.
Nothing errored, because that is exactly what the gate's fail-open looks like:
agentIdForRun returns null for a context with no agentId , indistinguishable from a CLI run or a system jobresolveConnectionScopeForRun therefore degrades to inherit, which is correct: a missing answer must not read as a permissions boundaryinherit allows everythingSo the two gated mail tools this executor is granted (search-live-emails, read-live-email-thread, plus fetch-email-attachments on the agent's own tool set) read the whole live mailbox however narrowly the drafter was scoped on its Connections screen. The permission looked enforced and was not.
Fix: set agentId on the sub-context, beside the ids it already carries, when a real drafter resolved. An executor running on defaults has no agent identity to enforce against, so it keeps inheriting.
Two other fences ride the same key and were also answering about nobody , both now correct:
files: grant on read-document, same absent-means-inherit shapeupdateConversationFieldsTool → buildFieldPermissions. This one was failing closed: ownerId !== runningAgentId forbade every owned field, including a field owned by this very drafter. Naming the run fixes that in the direction the owner intended. This is a behaviour change beyond the reported bug and is called out here deliberately.read-live-email-thread returned each message's bcc list as ordinary participant data, and its own description tells the model to reuse the returned recipient lists when asked to include everyone on a thread. draft-email merges an agent-chosen recipient into the visible To.
Gmail keeps the Bcc header only on the user's own sent messages , so the one case where the tool could return a bcc at all is the case where returning it does damage: the sender's deliberately hidden recipient, disclosed to the visible ones, in an email a human reviewer has no reason to question because the addresses came off the thread.
Nothing downstream can undo it , a recipient list carries no provenance by the time it reaches the draft , so it is enforced at the only place the distinction still exists: the tool stops emitting it. mail's read-thread action derives its payload from this schema, so it follows automatically.
live-thread-search-eval.ts read draft-email's recorded arguments through a hand-written cast, against the repo's no-cast rule. The shape is now declared once (DraftEmailArgs), used by the stub that validates it and parsed by the scoring that reads it. A changed payload stops the run instead of being scored through a stale type as an abstention , which is one of the answers this eval grades, so the failure would have been invisible.
never returns Bcc recipients, even when the message carries them , asserts the whole result, then that the address appears under no key, so a later edit folding it into to/cc fails tooallows that same call on a run carrying NO agent id , the fail-open, stated , the bug expressed as behaviour, paired with the existing refusal testagentId and orgId, source-read for the reason that file's header already documentsvitest run over the 5 affected files , 60 passedpnpm --filter @zero/server run types , exit 0pnpm --filter @zero/server run types:test , no errors in any touched file; the failures it reports are pre-existing in src/trpc/routes/__tests__/ (`Libra has not measured any production surfaces for this change yet.