Skip to content

feat(google): let gmail send and gcal book, on one reconnect - #86

Merged
fajarhide merged 1 commit into
mainfrom
feat/google-writes
Aug 24, 2026
Merged

feat(google): let gmail send and gcal book, on one reconnect#86
fajarhide merged 1 commit into
mainfrom
feat/google-writes

Conversation

@fajarhide

Copy link
Copy Markdown
Owner

Six write tools: send_message, modify_message and trash_message on gmail, create_event, update_event and delete_event on gcal. Both scopes move in the same change on purpose, because one google grant backs all three prefixes and doing them separately would make every existing Google connection reconnect twice.

gmail.readonly becomes gmail.modify, which carries send, label changes and trash in one scope. That is the same restricted tier gmail.readonly already sat in, so nothing about Google's verification changes. gcal keeps calendar.readonly and adds calendar.events beside it, because calendar.events does not grant calendarList.list and dropping the readonly scope would break list_calendars.

Two executor changes the manifests needed: a base64url argument type, since messages.send takes raw in the url-safe alphabet and the base64 validator rejected - and _; and the fake upstream can now model a real 204, which it could not construct with a body.

Closes #84

Evidence

Every field named here appears in a response example in Google's reference, checked against the docs rather than recalled: Message for the gmail three (id, threadId, labelIds), Events for create and update, and delete answers with no body so it declares no fields. gmail.modify covering messages.send and calendar.events not covering calendarList.list were both read off the scope lists on those endpoint pages.

Four tests guard the four things inferred rather than transcribed. Broken deliberately: the standard-to-url-safe conversion removed, param: 'start.dateTime' flattened to 'start', and calendar.readonly dropped.

 × asks for a different scope per prefix, which is why the union matters
   → expected Set{ …(3) } to deeply equal Set{ …(4) }
 × keeps calendar.readonly beside calendar.events, because list_calendars needs it
   → expected [ Array(1) ] to include 'https://www.googleapis.com/auth/calen…'
 × nests a start time under start.dateTime from a flat argument
   → expected { summary: 'Standup', …(2) } to deeply equal { summary: 'Standup', …(2) }
 × converts a standard base64 raw message to the url-safe alphabet Gmail wants
   → expected { raw: 'YWI+Y2Q/ZQ==' } to deeply equal { raw: 'YWI-Y2Q_ZQ' }
   Tests  4 failed | 54 passed (58)

Restored, full suite:

 Test Files  42 passed (42)
      Tests  442 passed (442)

What I did not do

Not called against a real Google account. Every check above is against the fetch double, so maturity stays where it was rather than being promoted on my own say-so, and the six tools are unproven against the vendor. The reconnect this forces is also untested against a live grant.

Attendees are left out of create_event: they are an array of objects and ArgDef holds an array of strings, so the manifest cannot express them. That is an executor gap and wants its own issue rather than a workaround in one provider.

create_event takes an RFC3339 stamp with an offset instead of a separate time zone argument, because one argument cannot fill both start.timeZone and end.timeZone.

gmail could read a thread it needed to answer and never answer it, and gcal
could read a calendar it could not book. Six tools close that: send_message,
modify_message and trash_message on gmail, create_event, update_event and
delete_event on gcal.

Both scopes move in the same change because one google grant backs all three
prefixes, so doing them separately would make every existing Google connection
reconnect twice instead of once.

gmail.readonly becomes gmail.modify, which carries send, label changes and
trash in one scope. It is the same restricted tier gmail.readonly already sat
in, so nothing about Google's verification changes. gcal keeps calendar.readonly
and adds calendar.events beside it: calendar.events does not grant
calendarList.list, so dropping the readonly scope would break list_calendars.

Two executor changes the manifests needed. A base64url argument type, because
messages.send takes raw in the url-safe alphabet and the base64 validator
rejected - and _; it now accepts either alphabet, refuses a mix, and converts
the standard one so a model that reaches for base64 still sends mail. And the
fake upstream can now model a real 204, which it could not construct with a
body, which is what a delete actually answers.

Every field named here appears in a response example in Google's reference:
Message for the gmail three, Events for create and update, and delete answers
with no body at all so it declares no fields.

Attendees are left out of create_event. They are an array of objects and ArgDef
can hold an array of strings, so the manifest cannot express them yet.

Closes #84
@fajarhide fajarhide self-assigned this Aug 24, 2026
@fajarhide
fajarhide merged commit d151556 into main Aug 24, 2026
2 checks passed
@fajarhide
fajarhide deleted the feat/google-writes branch August 24, 2026 08:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

gmail and gcalendar are read only, and the verification wall they look blocked on is already behind them

1 participant