Libra CodeHub

CedarCopilot/cedar-mail

Branch: staging

fix(meetings,tasks): route meetings by attendee; stop agents closing user tasks

merged#2605CedarCopilot

CedarCopilot wants to merge 8 commits into staging from fix/sherlock-granola-routing-task-guards

Live on prod, no production signal yetTimeline and evidence
  1. Opened
    Aug 27, 2026, 9:38 AM
  2. Aug 27, 2026, 12:46 PM
  3. Merged
    Aug 27, 2026, 1:16 PM
  4. Live on prod
    Aug 27, 2026, 1:16 PM
  5. Observed 11 hours
    Aug 27, 2026, 1:16 PM
  6. Pipelines steady after this deploy
    Aug 27, 2026, 1:16 PM
  7. Unobserved

    Live on prod, no production signal yet

    Aug 28, 2026, 12:36 AM

Behaviors Libra is checking

Meeting import behavior from cron/process-meeting-background-sync.ts keeps working in prod.Inconclusivelow confidence

Strict CloudWatch fallback saw 13,970 success-shaped log lines matching calendar, meeting, meetings, but no tied operation was present, so Libra is not calling this working.

prod, checked Aug 28, 2026, 12:36 AM

Libra has verdicts on 0 of 1 tracked behaviors on prod; 1 is still being checked. Libra checks hourly for 3 days after each deploy.

Two Sherlock investigations from 2026-08-27, both reported in Slack.

1. Meetings were filed by API key holder, not by who attended

gong, fireflies, fathom and circleback all match a payload's participants against Cedar accounts before routing. granola did not: it authenticated a shared secret, then trusted an X-User-Id header naming the key's owner.

With a personal Granola key the key owner is also the attendee, so the difference never showed. With a workspace key it is not: every note the key can read arrives stamped with the key owner's id. The reporter accumulated ten deals for clients he had never met, and his own write-up called it exactly right ("Cedar has no attendee check").

The webhook was not the important half. process-meeting-background-sync.ts lists every meeting a connection's key can read and attributes all of them to that connection's owner, with no attendee check anywhere. Fixing only verifyGranolaRequest would have left the actual ingestion path untouched , this was caught reviewing the first version of this branch, not in the original diagnosis.

New services/integrations/meetings/attendee-routing.ts is the single answer to "whose meeting is this", used by both paths:

  • webhook , granola resolves the owner from attendees; a meeting with no Cedar attendee is rejected rather than parked on the key holder
  • pull , the background sync re-routes a meeting off the key owner when they did not attend, and skips it when no Cedar attendee matches

Both scope the lookup to the key owner's organization, so a shared workspace can never route across tenants. A workspace key now fans out correctly: every teammate who attended and has a Cedar account gets the meeting, whether or not they hold their own Granola connection.

Deliberate conservatism on the pull path

When a payload carries no attendee emails at all, the sync keeps today's owner-attribution instead of dropping the meeting. Krisp routes on domains and some payloads arrive bare; skipping those would trade one silent data problem for another. Only a payload that has attendees and doesn't include the key owner gets re-routed or skipped.

2. The agent was closing and deleting hand-written tasks

One next-steps run marked two user-created tasks done and deleted a third 23ms apart, after judging the third "obsolete as a user action" because a counterparty said they would handle it. The user never asked for any of it.

cancelTaskTool already promised this protection in its own description ("Defaults to deleting only agent-created tasks") but enforced it only in bulk mode. The explicit-taskIds path had no such check, and updateTaskTool had none at all.

New tools/task/user-authored-task-guard.ts is the shared rule, applied to both.

The guard keys on "did a person ask for this in this turn", not on approvalMode , that axis files Slack/SMS/iMessage bots and external MCP as autonomous even though a human typed the request, and refusing those would be a new bug. What's left over is exactly the background tier that caused this one.

Scoped to status on updateTaskTool: enriching a user's task with a due date, notes or a draft stays allowed. Closing it is not the agent's call.

Refusals reuse each tool's existing "accepted but not written" shape (success: true, changed: false; protected ids returned in protectedTasks) so respecting the guard does not page #cedar-monitoring.

Same class, not fixed here

  • fellow.ts:108 and clari.ts:93 carry the identical header-only routing , both say so in their own comments ("Mirrors verifyGranolaRequest"). The reporter has a Fellow connection too, so he is still exposed through that provider.
  • integrations.ts:2258 (manual sync) attributes to the requested user by the same pattern.

Worth doing as a follow-up; kept out of here to keep the change reviewable.

Verification

  • tsc --noEmit clean
  • 227 tests pass across the tou
Show production surfaces and changed-file mapping

Production surfaces

Libra has not measured any production surfaces for this change yet.

Changed files → surfaces

  • apps/server/src/cron/process-meeting-background-sync.tsno production surface mapped
  • apps/server/src/mastra/tools/task/__tests__/user-authored-task-guard.test.tsno production surface mapped
  • apps/server/src/mastra/tools/task/cancelTaskTool.tsno production surface mapped
  • apps/server/src/mastra/tools/task/listTasksTool.tsno production surface mapped
  • apps/server/src/mastra/tools/task/updateTaskTool.tsno production surface mapped
  • apps/server/src/mastra/tools/task/user-authored-task-guard.tsno production surface mapped
  • apps/server/src/mastra/utils/approval.tsno production surface mapped
  • apps/server/src/services/integrations/meetings/__tests__/granola-attendees.test.tsno production surface mapped
  • apps/server/src/services/integrations/meetings/attendee-routing.tsno production surface mapped
  • apps/server/src/services/integrations/meetings/granola.tsno production surface mapped