feat(agent-cal): connection-based unified calendar API, SDK, and CLI with connect command#28297
feat(agent-cal): connection-based unified calendar API, SDK, and CLI with connect command#28297sahitya-chandra wants to merge 18 commits intomainfrom
Conversation
- Backend: GET /v2/calendars/connections and connection-scoped events/freebusy
- GET/POST /connections/{connectionId}/events, GET/PATCH/DELETE .../events/{eventId}, GET .../freebusy
- CredentialsRepository.findCredentialByIdAndUserId, GoogleCalendarService methods by credentialId
- SDK: getConnections(), all event/freebusy methods take connectionId (not calendar type)
- CLI: status shows connectionIds; events list uses --connection-id or first connection
- MCP: list_connections tool; all tools use connection_id (optional, defaults to first)
- Deprecation note for GET /:calendar/event/ (prefer /events/ and connection-scoped)
PR Review — Agent Cal Implementation vs PlanI've reviewed the full diff against the implementation plan we iterated on. Here's my assessment: Plan Coverage
Issues Found1. PATCH backward compatibility gapThe original controller already has dual @Get("/:calendar/event/:eventUid")
@Get("/:calendar/events/:eventUid")But Recommendation: Verify if the legacy PATCH was ever singular ( 2.
|
- Add dual @patch decorator for legacy singular /event/ backward compat - Remove @isemail on CalendarConnectionItem.email (conflicts with 'unknown' fallback) - Broaden OAuth scope to READ_PROFILE READ_CALENDARS WRITE_CALENDARS - Extract shared private helpers in GoogleCalendarService (DRY) - Extract ConnectedCalendarEntry interface in controller (remove inline types) - Handle MCP notifications/initialized as no-op instead of error - Fix build script to use tsup.config.ts (includes MCP server entry)
…edential file permissions - Add escapeHtml() helper to sanitize user-controlled values in HTML responses - Apply escapeHtml() to error query param and token exchange error messages - Set mode 0o600 on credentials.json to prevent other local users from reading tokens
READ_CALENDARS and WRITE_CALENDARS don't exist in Cal.com's AccessScope enum. The only valid scopes are READ_BOOKING and READ_PROFILE. Calendar CRUD works via the unified calendar API endpoints which check token validity, not specific calendar scopes.
- Parse OAuth scope by whitespace (RFC 6749) in authorize view - Allow CONFIDENTIAL clients to use PKCE without client_secret - Derive token URL from authorize URL when set; pass CAL_API_BASE_URL in CLI - Web token route: accept JSON, return error_description - API v2: validate web-issued JWTs (CALENDSO_ENCRYPTION_KEY) when not in DB
Devin AI is addressing Cubic AI's review feedbackA Devin session has been created to address the issues identified by Cubic AI. |
… remove JWT revocation bypass - OAuthService.ts: Revert validateClient to always require client_secret for CONFIDENTIAL clients. PKCE supplements but never replaces client authentication per RFC 6749 §2.3. Previously, passing any code_verifier allowed skipping client_secret verification entirely. - tokens.repository.ts: Remove decodeWebOAuthToken JWT fallback from getAccessTokenExpiryDate, getAccessTokenOwnerId, and getAccessTokenClient. When a token is revoked (deleted from DB), the JWT fallback would still accept it as valid until natural expiry, bypassing revocation. Web-issued tokens must be stored in the accessToken table so deletion equals revocation. Co-Authored-By: bot_apk <apk@cognition.ai>
…mData The route was changed to import parseRequestData but the test mock still referenced parseUrlFormData, causing all 13 token endpoint tests to fail with 500 errors. Co-Authored-By: bot_apk <apk@cognition.ai>
- P0: cmdToken() writes token to stdout via process.stdout.write (no log prefix) - P1: MCP parse-error sends null ID per JSON-RPC spec (was undefined) - P1: Add @isdefined() on start/end fields in CreateUnifiedCalendarEventInput - P1: Token route falls back to form-urlencoded when Content-Type missing (RFC 6749) - P2: email field on CalendarConnectionItem is now nullable (was 'unknown' sentinel) - P2: deleteEvent checks for status:'error' responses - P2: getDefaultGoogleCalendarId continues searching when connection has no calendar - P2: base64UrlEncode throws instead of returning empty string when no encoder - P2: README error example uses connId instead of 'google' - P2: timeZone validated with @IsTimeZone(); to-before-from cross-field validation - P2: OAuth page preserves array query params via append() - P3: Fixed misleading JSDoc on needsRefresh()
Devin AI is addressing Cubic AI's review feedbackNew feedback has been sent to the existing Devin session. |
…rseRequestData/parseUrlFormData Co-Authored-By: bot_apk <apk@cognition.ai>
- Remove raw token output from CLI (security: avoid logging sensitive info) Replaced 'token' command with 'token-info' that shows masked token + expiry - Freebusy validator throws BadRequestException for invalid date ranges instead of returning false, keeping API error response structure consistent
…ager.devin.ai/proxy/github.com/calcom/cal.com into feat/agent-cal-connection-based-api
Cubic AI Feedback — AddressedBoth high-confidence issues identified by Cubic AI have been fixed in commit
✅ Pushed commit |
|
Cubic AI review feedback — addressed issues:
Skipped issue:
Test fix:
|
…ettings Opens the user's Cal.com instance calendar settings page in the browser so they can connect Google, Outlook, or Apple Calendar. Derives the URL from NEXT_PUBLIC_WEBAPP_URL or CAL_OAUTH_AUTHORIZE_URL env vars, falling back to https://app.cal.com. Also updates 'status' no-connections message to suggest 'connect' command.
…ager.devin.ai/proxy/github.com/calcom/cal.com into feat/agent-cal-connection-based-api
All-day Google Calendar events use start.date instead of start.dateTime. The previous filter (e.start?.dateTime != null) silently dropped them. Changes: - GoogleCalendarEventResponse interface: dateTime/timeZone now optional, added date field - listEventsWithClient: filter accepts events with either dateTime or date - Output pipe transformDateTimeWithZone: converts date-only to midnight UTC
apps/api/v2/src/modules/cal-unified-calendars/inputs/freebusy-unified.input.ts
Show resolved
Hide resolved
Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
There was a problem hiding this comment.
1 issue found across 2 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/api/v2/src/modules/cal-unified-calendars/pipes/get-calendar-event-details-output-pipe.ts">
<violation number="1" location="apps/api/v2/src/modules/cal-unified-calendars/pipes/get-calendar-event-details-output-pipe.ts:129">
P2: Don’t default the timezone to "UTC" when Google omits `timeZone`; if the `dateTime` already carries an offset, forcing UTC makes the output inconsistent and can shift client-side interpretation. Derive the offset from `dateTime` (or leave it unset) instead of hardcoding UTC.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
apps/api/v2/src/modules/cal-unified-calendars/pipes/get-calendar-event-details-output-pipe.ts
Show resolved
Hide resolved
Devin AI is addressing Cubic AI's review feedbackNew feedback has been sent to the existing Devin session. ✅ No changes pushed |
|
Cubic AI flagged 1 issue on the all-day events commit: Violation 1 (confidence 7/10): UTC timezone default in |
There was a problem hiding this comment.
2 issues found across 2 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/api/v2/src/modules/cal-unified-calendars/inputs/freebusy-unified.input.ts">
<violation number="1" location="apps/api/v2/src/modules/cal-unified-calendars/inputs/freebusy-unified.input.ts:11">
P2: Throw a BadRequestException here instead of returning false so the API uses the standard error response format.
(Based on your team's feedback about validators throwing BadRequestException on invalid input.) [FEEDBACK_USED]</violation>
</file>
<file name="apps/web/modules/auth/oauth2/authorize-view.tsx">
<violation number="1" location="apps/web/modules/auth/oauth2/authorize-view.tsx:41">
P1: Scope parsing now accepts comma-delimited scopes, which can incorrectly grant permissions from malformed OAuth scope input. Keep scope splitting space-only.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
apps/api/v2/src/modules/cal-unified-calendars/inputs/freebusy-unified.input.ts
Outdated
Show resolved
Hide resolved
Devin AI is addressing Cubic AI's review feedbackNew feedback has been sent to the existing Devin session. ✅ Pushed commit |
…space-only (RFC 6749)
What does this PR do?
Implements Agent Cal — a product enabling AI agents to connect to user calendars via Cal.com's verified OAuth infrastructure, eliminating the need for agent developers to set up their own Google/Outlook OAuth credentials.
4 Workstreams
1. Backend API — Unified Calendar API Extensions (
apps/api/v2/)GET /v2/calendars/connectionsendpoint returning all calendar connections withconnectionIdGET/POST/PATCH/DELETE /v2/calendars/connections/{connectionId}/events/*GET /v2/calendars/connections/{connectionId}/freebusyGET/POST/DELETE /v2/calendars/{calendar}/events,GET /{calendar}/freebusy@Get/@Patchdecorators for singular/event/(deprecated) and plural/events/ConnectedCalendarEntryinterface to eliminate inline type annotationslistEventsWithClient,createEventWithClient, etc.)@IsDefined()onstart/end,@IsTimeZone()on timezone fields, cross-fieldto >= fromvalidation2.
@calcom/agent-calSDK (packages/agent-cal/)AgentCalclient class (API key or OAuth token auth)getConnections(),listEvents(),createEvent(),getEvent(),updateEvent(),deleteEvent(),getFreeBusy()~/.agentcal/credentials.json)3. CLI Auth Flow (in
packages/agent-cal/src/cli.ts)npx @calcom/agent-cal auth— PKCE OAuth with localhost callback server (port 9876)npx @calcom/agent-cal connect— Opens Cal.com calendar settings in the browser to connect Google, Outlook, or Apple Calendarstatus,events list,token,disconnect,mcpREAD_BOOKING READ_PROFILE4. OAuth Landing Page (
apps/web/app/agent/oauth/page.tsx)append()(no silent collapsing)@calcom/atomsplanned for Phase 2Bonus: MCP Server (
packages/agent-cal/src/mcp/server.ts)notifications/initializedas no-op per MCP protocolnullID per JSON-RPC specUpdates since last revision
New: All-day events now included in listEvents response
listEventsWithClientfiltered events withe.start?.dateTime != null, which silently dropped all-day events (Google Calendar usesstart.dateinstead ofstart.dateTimefor all-day events)GoogleCalendarEventResponseinterface updated:dateTime/timeZonenow optional, addeddatefield onstart/endtransformDateTimeWithZonenow handles both timed events and all-day events, converting date-only (e.g."2026-03-20") to midnight UTC ("2026-03-20T00:00:00"with timezone"UTC")dateTimeordate:e.start?.dateTime != null || e.start?.date != nullPrevious:
connectCLI commandnpx @calcom/agent-cal connect— opens the user's Cal.com calendar settings page in the browser so they can add Google, Outlook, or Apple Calendar without manually navigating the URLNEXT_PUBLIC_WEBAPP_URLorCAL_OAUTH_AUTHORIZE_URLenv vars, falling back tohttps://app.cal.comstatuscommand now suggests runningconnectwhen no calendars are connected (instead of a generic URL)connectcommand documentationPrevious: Security & validation fixes:
0o600(owner read/write only) on~/.agentcal/credentials.jsonclient_secretfor CONFIDENTIAL clients; PKCE alone is insufficient@IsDefined()validators onstart/endevent fields (prevents partial event creation)@IsTimeZone()validation on timezone parameters (rejects invalid IANA timezones)to >= fromvalidation on date ranges via custom@Validate(IsAfterFrom)decoratorPrevious: Bug fixes & improvements:
authorize-view.tsxdeleteEventnow checks forstatus: "error"in response (was silently ignoring errors)getDefaultGoogleCalendarIdcontinues searching when first connection has no usable calendar (was returning null too early)base64UrlEncodethrows error instead of returning empty string when no encoder availablenullID per JSON-RPC 2.0 spec (was sendingundefined)URLSearchParams.append()(was collapsing to first value)cmdToken()writes token directly to stdout viaprocess.stdout.write(no log prefix for piping)connIdinstead of"google"stringPrevious: API shape changes:
CalendarConnectionItem.emailis nowstring | null(wasstring!with"unknown"fallback) — BREAKING for consumers expecting always-present email field@IsEmail()validator fromemailfield (allows null values)READ_CALENDARS WRITE_CALENDARSto valid Cal.com scopesREAD_BOOKING READ_PROFILEerror_descriptionfield per RFC 6749parseRequestData)Previous: Code quality:
ConnectedCalendarEntryinterface in controller (replaced inline type annotations)GoogleCalendarServiceto DRY up user-scoped vs connection-scoped methodsneedsRefresh()functiontsup.config.ts(includes MCP server entry point)Important items for human review
emailfield now nullable —CalendarConnectionItem.emailchanged fromstring!(always present) tostring | null. Consumers expecting.emailto always be a string will need updates.start.dateinstead ofstart.dateTime) are now converted to midnight UTC. For example, a March 20 all-day event becomes"2026-03-20T00:00:00"with timezone"UTC". Google'send.dateis exclusive (next day), so it appears as"2026-03-21T00:00:00 UTC". Consumers should be aware that there's no explicitallDayboolean flag — they must infer this from the00:00:00time and UTC timezone pattern.connectedCalendars as ConnectedCalendarEntry[]. IfCalendarsService.getCalendars()returns a different shape, runtime errors may occur. Consider adding runtime validation.@calcom/atoms) is Phase 2.connectcommand URL derivation: Constructs web app URL from env vars with multiple fallbacks. If env vars are partially set or misconfigured, could redirect to wrong URL.Mandatory Tasks (DO NOT REMOVE)
How should this be tested?
Prerequisites
http://localhost:9876/callbackAGENT_CAL_CLIENT_IDenv var or--client-idflagconnectcommand below)Test flow (CLI)
Test flow (API)
Test validation edge cases
Environment variables
AGENT_CAL_CLIENT_ID— OAuth client ID (required for auth)CAL_OAUTH_AUTHORIZE_URL— Override authorize URL (default: https://app.cal.com/auth/oauth2/authorize)CAL_OAUTH_TOKEN_URL— Override token URL (default: https://api.cal.com/v2/auth/oauth2/token)CAL_API_BASE_URL— Override API base URL (default: https://api.cal.com)NEXT_PUBLIC_WEBAPP_URL— Override web app URL forconnectcommand (default: https://app.cal.com)Link to Devin Session: https://app.devin.ai/sessions/eea91e3180b7429d9b39a3bb36028315
Requested by: @sahitya-chandra