Skip to content

Add Gmail tools for Interchange - #1

Merged
TheGreatAxios merged 10 commits into
mainfrom
add-gmail-tools
Sep 22, 2026
Merged

TheGreatAxios merged 10 commits into
mainfrom
add-gmail-tools

Conversation

@pratikbuilds

@pratikbuilds pratikbuilds commented Aug 23, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • add a focused Gmail REST client with strict response handling
  • expose ten Interchange Gmail tools with approval-gated mutations
  • add separate read and mutation live suites with safe cleanup

Live Gmail validation

The full live suite passed against a dedicated Gmail fixture account and exercised every exposed tool:

  • gmail_search_threads finds the one-message fixture thread.
  • gmail_get_thread and gmail_get_message retrieve fixture metadata.
  • gmail_list_labels lists mailbox labels.
  • gmail_create_draft and gmail_list_drafts create and find a disposable draft; the suite deletes it during cleanup.
  • gmail_label_message and gmail_unlabel_message add and remove STARRED on the fixture message.
  • gmail_label_thread and gmail_unlabel_thread add and remove STARRED across the fixture thread, then restore its original label state.

The live suite never sends email.

Verification

  • bun run test — 31 passed
  • bun run typecheck
  • bun run typecheck:live
  • bun run build
  • full Gmail live suite — 10 passed, 0 failed

@TheGreatAxios
TheGreatAxios self-requested a review August 24, 2026 22:13

@TheGreatAxios TheGreatAxios left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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; encodeURIComponent on all IDs; per-request timeout composed with caller abort signals.
  • Live-test OAuth harness done right: loopback callback with state validation and timeout, token files written 0600 in 0700 dirs, 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.

@TheGreatAxios

Copy link
Copy Markdown
Contributor

Review

Verdict: request changes. Client, sidecar, and approval split look solid. Earlier notes are addressed (live tests skip instead of throw, failed credential resolution retries, resultCountEstimate is a number, mutations declare approval: "ask" from one catalog). Two items I'd like on this PR.

Should-fix

  1. Reply drafts take the first <...> from From as To. plainEmailAddress in src/tools/create-tools.ts uses /<([^<>]+)>/. Sequence: gmail_create_draft with replyToMessageId and no to, original From "Ada <nickname>" <ada@example.com> → raw To: nickname. The quote-aware splitter in src/tools/models.ts already exists; reuse that, or take the last angle-addr, for reply targeting.

  2. Live thread-label checks pass when messages is empty. tests/live/gmail.mutations.e2e.test.ts uses [].every(...), which is true. The unit test mocks threads.modify as { id, messages: [] } and only asserts isError. If Gmail omits messages, gmail_label_thread returns { id, messages: [] } and live still goes green. Assert messages.length > 0 and that each message carries (then drops) STARRED.

Confirm

Label tools forward TRASH / SPAM (LabelIds is any non-empty string[]). Google MCP sends those through a sensitive tool. Approval is ask, so this may be intentional — a yes/no is enough.

Also

Tool errors surface Gmail API request failed (404 Not Found) and drop GmailApiError.body, so Gmail's error.message never reaches the agent. The client already keeps the body.

Public API

Sidecar defineTool declarations are name + approval (Interchange ToolDeclaration). Schemas and descriptions live on createGmailTools().definitions. Five mutations are approval: "ask". Handle gmail-api and scope gmail.modify match package.json.

Checks

No GitHub status checks on add-gmail-tools. Locally: bun run typecheck, bun run typecheck:live, bun run build, bun run test (36 pass). Bare bun test skips live e2e when env is unset. Live Gmail suite not re-run here.

#2 (Node 24 + CI) stacks on this branch; CI lands there.

@TheGreatAxios TheGreatAxios left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review · Approve

Gmail API client and Interchange tool bundle (search/get thread/message, list/create drafts, label/unlabel message/thread, list labels), authenticated only through the mediated gmail-api credential — no raw tokens ever handled directly.

No findings.

Notes

  • Header injection in draft composition (src/tools/drafts.ts) is guarded: to/cc/bcc/subject/In-Reply-To/References all go through assertSafeHeader, which rejects embedded CR/LF.
  • Thread lookups in gmail_search_threads are capped at 10 concurrent requests (src/tools/create-tools.ts), and the drafts-by-query join terminates once all matched message IDs are found rather than paging every draft.
  • Credential resolution correctly clears its cached promise on failure so a transient credential error doesn't poison later calls.
  • Live Gmail tests are isolated under tests/live/, excluded from the published package, and gated behind GMAIL_LIVE_TEST=1; the token cache path is gitignored.

@TheGreatAxios
TheGreatAxios merged commit 88fafe9 into main Sep 22, 2026
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.

2 participants