Add Gmail tools for Interchange - #1
Conversation
TheGreatAxios
left a comment
There was a problem hiding this comment.
Review: add-gmail-tools -> main
Verdict: Comment (no blocking defects). Well-structured, defensively-written PR with real test coverage. One developer-experience bug worth fixing before/after merge, plus non-blocking observations.
Verification performed
bun run typecheck: clean (exit 0)bun run test(scoped script): 31 pass / 0 fail- Bare
bun test(the command README documents): exit 1 out of the box — see Finding 1 - Commits audited: 5, all on-scope for Gmail tooling; no stray/binary changes beyond expected
bun.lock
Findings
1. [Low] Fresh contributor following the README gets a failing suite.
README documents bare bun test, which sweeps in tests/live/gmail.read.e2e.test.ts; its opt-in gate throws rather than skips (tests/live/gmail.read.e2e.test.ts:7-12), so bun test exits 1 with no env config. The sibling mutations suite gates correctly with test.skip (tests/live/gmail.mutations.e2e.test.ts:10-14). Two consistent fixes: make the read gate skip like its sibling, or document bun run test (the scoped package.json script already excludes both live e2e files).
2. [Observation] Failed client resolution is memoized.
In createGmailTools, clientPromise ??= caches the promise even when the first credential resolution rejects (src/tools/create-tools.ts:398-411). Every later call keeps returning the stale rejection until dispose/recreate, even if credentials become resolvable again. Fail-closed is correct; consider clearing clientPromise on rejection so recovery is possible.
3. [Observation] Query-filtered draft listing scans all drafts.
listDraftsForQuery pages through the entire draft collection at pageSize: 500 on every call (src/tools/create-tools.ts:157-169), independent of the requested page size. Fine at typical mailbox scale; worth an early-exit once enough matches are collected for the requested page.
4. [Observation] Thread search fan-out.
gmail_search_threads issues one getThread per result thread concurrently, capped at 50 by the input schema. Inherent to Gmail's API shape; just noting the latency profile (up to 51 upstream requests per tool call).
5. [Question] resultCountEstimate is stringified.
String(response.resultSizeEstimate) turns Google's numeric field into a string (src/tools/create-tools.ts:220). Intentional Interchange-side contract, or accidental?
Strengths
- Strict arktype inputs (
"+": "reject") across every tool, with generated JSON Schemas and a runtime definitions-vs-handlers invariant check. - CRLF injection guards on all header-bound values, including reply references (
src/tools/drafts.ts); quote-aware RFC 5322 recipient splitting with dedicated tests (src/tools/models.ts). - SSRF guard rejecting absolute request paths at the client boundary;
encodeURIComponenton all IDs; per-request timeout composed with caller abort signals. - Live-test OAuth harness done right: loopback callback with
statevalidation and timeout, token files written0600in0700dirs, bearer pinned to the Gmail origin, 401 refresh limited to idempotent methods, concurrent-refresh dedupe. - Every mailbox mutation requires approval (
MUTATING_TOOL_NAMES->approval: "ask"), and a unit test enforces it.
Summary
Live Gmail validation
The full live suite passed against a dedicated Gmail fixture account and exercised every exposed tool:
gmail_search_threadsfinds the one-message fixture thread.gmail_get_threadandgmail_get_messageretrieve fixture metadata.gmail_list_labelslists mailbox labels.gmail_create_draftandgmail_list_draftscreate and find a disposable draft; the suite deletes it during cleanup.gmail_label_messageandgmail_unlabel_messageadd and removeSTARREDon the fixture message.gmail_label_threadandgmail_unlabel_threadadd and removeSTARREDacross the fixture thread, then restore its original label state.The live suite never sends email.
Verification
bun run test— 31 passedbun run typecheckbun run typecheck:livebun run build