Libra CodeHub

CedarCopilot/cedar-mail

Branch: staging

fix(caretta): route webhooks by attendee, make signing optional, restore logs

merged#2572CedarCopilot

CedarCopilot wants to merge 1 commit into staging from fix/caretta-webhook-attendee-routing

Live on prod, no production signal yetTimeline and evidence
  1. Opened
    Aug 24, 2026, 5:52 PM
  2. Aug 24, 2026, 6:00 PM
  3. Merged
    Aug 24, 2026, 6:12 PM
  4. Live on prod
    Aug 24, 2026, 6:12 PM
  5. Observed 3 days
    Aug 24, 2026, 6:12 PM
  6. Pipelines steady after this deploy
    Aug 24, 2026, 6:12 PM
  7. Unobserved

    Live on prod, no production signal yet

    Aug 27, 2026, 6:05 PM

Behaviors Libra is checking

Caretta MCP back-sync meetings preserve the provider's `eventId` as the Google Calendar event ID so historical calls can link to their calendar events.Brokenlow confidence

OTEL fallback found no prod spans matching carettaDriver.listMeetings / caretta_list_my_calls since deploy.

prod, checked Aug 27, 2026, 6:05 PM
Caretta webhook meetings use the call's actual start time (`start`, `started_at`, or `start_time`) before falling back to event emission time, so delayed notes events can match the correct calendar meeting and deal.Workinghigh confidence

Since deployment at 2026-08-25T01:12:27Z, production CloudWatch shows 48 '[caretta] incoming webhook' and '[caretta] event parsed' executions. There are 27 '[caretta] mapped to Cedar meeting' successes across 19 distinct external calls and 3 organizers; all 27 successful.

prod, checked Aug 26, 2026, 7:54 PM
Caretta webhook meetings use supplied event ID fields to link the recording to its Google Calendar event.Inconclusivemedium confidence

Since 2026-08-25T01:12:27Z, CloudWatch for production api-service recorded 48 `[caretta] incoming webhook` and 48 `[caretta] event parsed` logs. Of these, 27 were verified and emitted `[caretta] mapped to Cedar meeting`; 21 were explicitly rejected as `[caretta] no Cedar user.

prod, checked Aug 26, 2026, 7:44 PM
Caretta webhook deliveries are accepted without a signature when none of the matched attendees' organizations has configured a Caretta webhook secret, while any configured secret makes a valid signature mandatory.Inconclusivehigh confidence

Since 2026-08-25T01:12:27Z, CloudWatch recorded 48 `[caretta] incoming webhook` and 48 parsed events. Of these, 27 attendee-matched deliveries logged both `[caretta] no org signing secret — accepting on attendee match alone (unauthenticated)` and `[caretta] verified` with.

prod, checked Aug 26, 2026, 7:50 PM
Caretta webhook ingestion and MCP back-sync diagnostics are emitted to stdout as structured logs as well as the existing log sink, restoring CloudWatch visibility into received payloads, routing, mapping, and provider failures.Not checked
prod, checked Aug 26, 2026, 7:39 PM
Caretta webhooks route a meeting to every matched Cedar attendee, including participants when the call owner does not have a Cedar account, resolving attendees by connected Google mailbox or Cedar login email.Not checked
prod, checked Aug 26, 2026, 7:50 PM

Libra has verdicts on 1 of 6 tracked behaviors on prod; 5 are still being checked. Libra has 1 low-confidence broken verdict it is still confirming. Libra checks hourly for 3 days after each deploy.

What happened

Caretta started pushing call events to /webhooks/meeting-notes/caretta on 2026-08-24. All 77 deliveries that day were rejected, so no meeting reached Cedar and no post-meeting recap could be drafted for Rhocar at Corgi.

ReasonCount
no_user_found_for_owner_email43
webhook_secret_not_configured34

Both return 401, which is why Caretta kept retrying , those 77 are retry ladders, not 77 distinct calls.

Three causes

1. Routing was owner-only. verifyCarettaRequest resolved data.call.owner.email and gave up if that address had no Cedar account. At Corgi only rhocar@ and <email> have accounts; Kian, Eddy, Moksh, Omar and Simon do not. Every call a teammate organized was dropped even with a Cedar user in the room.

Now matches every attendee (owner and participants) via a Google connection.email then user.email, mirroring findUserIdsFromCirclebackPayload, and returns userIds. The multi-user fan-out plumbing already existed through MeetingVerificationResultverifyAndParseMeetingWebhookRequest → the webhook handler, so no shared code changed. Batched into two inArray queries rather than per-attendee lookups, since this runs before any signature check.

2. A signing secret was mandatory, and no org has ever had one. Zero rows in organizations.provider_configs carry a caretta key. Signing is now optional.

Security note: secrets are resolved across every matched user's org, not just userIds[0]. Resolving only the first would let a caller pick which org's secret applies simply by ordering the participants, so adding one attendee from a secret-less org would skip verification for an org that had configured one. If any matched org has a whsec_, a valid signature is mandatory.

With no secret configured anywhere, this endpoint is unauthenticated: the payload, and therefore the attendee list it routes by, comes from the caller. Attendee matching is routing, not authentication. That is a deliberate trade to unblock a customer, documented at the call site, and closed by configuring the org secret.

3. The driver's logs reached neither sink. Every diagnostic called createStructuredLog bare, which returns a string and fire-and-forgets to Axiom without writing stdout. Nothing was in CloudWatch, and nothing landed in Axiom either, so owner_email / call_id / event type were unrecoverable , the exact fields needed to diagnose the failure. All sites now emit to both.

Two more that would have made a delivered meeting land wrong

  • meetingTime came from the envelope's occurred_at, which is when Caretta emitted the event, not when the call started. A notes_ready firing after the call pushes the meeting past the ±2 minute window findConversationFromMeetingCalendarLink uses to reach the calendar entry and, through it, the deal. The MCP mapper already did this correctly (toDate(call.start ?? call.createdAt)); only the webhook path didn't.
  • googleCalendarEventId was never set on either path, despite CarettaMcpCall.eventId being modeled and left unread, so crm_meeting_events.calendar_event_id stayed null.

Both now read from the call object. The webhook wire shape is not yet confirmed against a live payload, so several plausible key spellings are accepted and the key that actually supplied the value is logged as meeting_time_source / google_calendar_event_id_source. One real call turns both inferences into facts.

Also

Removes no_user_found_for_owner_email from expectedFailureReasons in app.ts; caretta was its only producer. The replacement no_users_found_matching_participants is already on that list, so the Slack alert spam from the 34 daily rejections stops.

Known, not addressed here

  • Multi-event dedup. Caretta emits up to four events per call sharing one call.id, and meetingHasValidExecution keys on dedupeExternalId = call.id, so the first event wins. If `call.co
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/http/app.tsno production surface mapped
  • apps/server/src/services/integrations/meetings/__tests__/caretta-webhook.test.tsno production surface mapped
  • apps/server/src/services/integrations/meetings/caretta.tsno production surface mapped