Skip to content

Gmail read tools: search, read a message, and download its attachments (no new scope needed) #711

Description

@serge-ivo

Part of the Gmail epic. No new OAuth scopegmail.readonly already covers everything here.

Why

findMatchingMessage (lib/gmail.ts:208) is the only reader in the codebase and it is shaped entirely around one job: find a confirmation link. It fetches ?format=full and then throws away everything that is not prose:

// lib/gmail.ts:69-75
function collectText(part: GmailPart | undefined): string {
	if (!part) return "";
	let out = "";
	if (part.body?.data && (part.mimeType === "text/html" || part.mimeType === "text/plain")) {
		out += decodeBody(part.body.data);
	}
	for (const child of part.parts ?? []) out += `\n${collectText(child)}`;

GmailPart (gmail.ts:62-66) declares only mimeType, body.data and parts. There is no filename, no body.attachmentId, and no call anywhere to /messages/{id}/attachments/{id}. So an agent cannot answer "what was attached to that email", which is the first step of every document-handling task.

It also returns only the single newest match (list.messages?.[0], gmail.ts:216-218) and truncates the body to 20 000 chars (gmail.ts:239) — right for link-hunting, wrong for "show me the thread I need to reply to".

Scope

Add to lib/gmail.ts (pure, unit-testable, no new deps):

  • extend GmailPart with filename? and body.attachmentId?
  • collectAttachments(part){ attachmentId, filename, mimeType, size }[]
  • listMessages(accessToken, query, max) → several matches, not just the newest
  • getMessage(accessToken, id) → headers incl. Message-ID/References, threadId, decoded body, attachment manifest
  • downloadAttachment(accessToken, messageId, attachmentId) → bytes

Then three connector tools on GMAIL_CONNECTOR, declared as a manifest in the GOOGLE_SHEETS_MANIFEST style (lib/connectors/google-sheets.ts:56), all scope: "read", mutates: false:

  • gmail_search — Gmail query syntax, returns id/threadId/from/subject/date/snippet/attachment names
  • gmail_read_message — full body + attachment manifest for one id
  • gmail_download_attachment — saves into the instance's own file store and returns the file id, so the bytes land where every other surface (RAG, fill_pdf_form, the send tool) already looks. It must NOT return raw base64 into the model's context.

Gate

Every one of these checks the owner's permissions.email flag before doing anything, the same precondition find_confirmation_link enforces at lib/storage-tools.ts:553:

if (!ctx.emailPermitted) {
	return fail(call.name, "Email access is not enabled for this agent.");
}

Declaring a Gmail tool in capabilities.tools must not be sufficient on its own — that would widen an owner-facing control silently for any agent that declares it.

Done when

  • an agent with Gmail connected and permissions.email on can find a message, read it, and land its PDF attachment in the instance file store
  • the tools are refused with a clear message when permissions.email is off, when Gmail is not connected, and when the deployment has no KEY_ENCRYPTION_KEY
  • unit tests cover the MIME walk against a real multipart-with-attachment payload fixture, including a nested multipart/alternative inside multipart/mixed

Metadata

Metadata

Assignees

No one assigned

    Labels

    backendBackend / Worker / API workconnectorsConnector + tool frameworkenhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions