You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A real email arrives with a PDF form attached ("return the completed Summer Competition form by 30 Aug"). The wanted behaviour: an agent reads the message, fills the attached form from the owner's stored details, and replies to the sender with the completed form attached.
PAGS cannot do any of the three steps today. This epic makes it possible.
What is actually there now (verified)
GMAIL_CONNECTOR is declared and works — lib/connectors/connected-accounts.ts:88, registered at lib/connectors/registry.ts:152. Token is a refresh token in user_api_keys(user_id, provider='gmail'), which is byte-for-byte what connectors/client.ts reads for any auth:"oauth" connector, so ctx.connectorClient("gmail") already resolves an existing connection with no migration.
It is declared read-only: scopes: { read: true, write: false } (connected-accounts.ts:91), and the granted OAuth scope is exactly one — GMAIL_SCOPE = "https://www.googleapis.com/auth/gmail.readonly" (lib/gmail.ts:13).
It declares tools: [] (connected-accounts.ts:110). The only Gmail-backed tool in the codebase is find_confirmation_link (lib/storage-tools.ts:208), gated at runtime by ctx.emailPermitted (storage-tools.ts:553), i.e. the owner's AgentState.permissions.email flag.
Live status on the operator account: GET /v1/email/status → {"connected":false,"email":null,"configured":true} — the OAuth client is configured in production, no mailbox connected yet.
The three gaps (verified)
1. Attachments are unreachable.findMatchingMessage (lib/gmail.ts:208) does fetch ?format=full, but the part walker only collects text:
and the GmailPart interface (gmail.ts:62-66) has no filename and no body.attachmentId, so nothing can even name an attachment, let alone fetch /messages/{id}/attachments/{id}. Closing this needs no new OAuth scope — gmail.readonly already covers attachment download.
2. There is no send path at all.grep -rn -i "gmail.send\|gmail.compose\|messages/send\|mailchannels\|sendgrid\|resend\.\|smtp\|nodemailer" workers/api/src packages/*/src returns zero hits outside one unrelated word ("resend" in a CLI prompt string, lib/connector-tool-prompt.ts:218). PAGS cannot send an email by any route. This one does need a new scope (gmail.send) and therefore a re-consent path for anyone already connected.
3. There is no PDF write path. The only PDF code is lib/resume-parse.ts, which sends a PDF to Claude as a document content block to READ it (resume-parse.ts:65). No pdf-lib, no form-field filling, nothing that produces a PDF.
Shape of the work
Four issues, in dependency order. Each is independently useful — (1) alone makes "what did that email say, and what was attached" answerable.
Console + docs surface for the new permission, consent and reconnect states
Design decisions taken up front
permissions.email stays a hard precondition for every Gmail tool, new ones included — not just find_confirmation_link. Otherwise an agent that declares gmail_search in capabilities.tools gains mailbox reach without the owner's existing toggle, which is a silent widening of an owner-facing control that exists today.
Tools go on the connector as a manifest, following GOOGLE_SHEETS_MANIFEST (lib/connectors/google-sheets.ts:56) — declarative params + handlers compiled by compileConnector. This supersedes the tools: [] rationale in the connected-accounts.ts header, which argued the registry had no grant model for find_confirmation_link's permission-flag gate. That argument is about that tool and stays true; it is not a reason for the connector to carry no tools at all. The header comment gets updated rather than left to contradict the code.
scopes.write must flip to true for send to be reachable at all: assertScope refuses a write-scoped tool on a connector declaring write:false (lib/connectors/client.ts:84-87). That flip is what makes the [connectors] Connector consent + write-scope safety + admin visibility #90 write-consent gate applicable, so send is consent-gated rather than ungated.
Minimal send scope.gmail.send is sufficient to reply in-thread (pass threadId plus In-Reply-To/References headers); gmail.modify is not needed and is not requested.
Not verified / open
Whether real-world club forms like the motivating one are AcroForms or flat scans. This decides how much of issue (2) is useful in practice; the fallback is designed for the flat case rather than assuming the good one. Not reproduced — needs the actual attachment.
The task that motivated this
A real email arrives with a PDF form attached ("return the completed Summer Competition form by 30 Aug"). The wanted behaviour: an agent reads the message, fills the attached form from the owner's stored details, and replies to the sender with the completed form attached.
PAGS cannot do any of the three steps today. This epic makes it possible.
What is actually there now (verified)
GMAIL_CONNECTORis declared and works —lib/connectors/connected-accounts.ts:88, registered atlib/connectors/registry.ts:152. Token is a refresh token inuser_api_keys(user_id, provider='gmail'), which is byte-for-byte whatconnectors/client.tsreads for anyauth:"oauth"connector, soctx.connectorClient("gmail")already resolves an existing connection with no migration.scopes: { read: true, write: false }(connected-accounts.ts:91), and the granted OAuth scope is exactly one —GMAIL_SCOPE = "https://www.googleapis.com/auth/gmail.readonly"(lib/gmail.ts:13).tools: [](connected-accounts.ts:110). The only Gmail-backed tool in the codebase isfind_confirmation_link(lib/storage-tools.ts:208), gated at runtime byctx.emailPermitted(storage-tools.ts:553), i.e. the owner'sAgentState.permissions.emailflag.GET /v1/email/status→{"connected":false,"email":null,"configured":true}— the OAuth client is configured in production, no mailbox connected yet.The three gaps (verified)
1. Attachments are unreachable.
findMatchingMessage(lib/gmail.ts:208) does fetch?format=full, but the part walker only collects text:and the
GmailPartinterface (gmail.ts:62-66) has nofilenameand nobody.attachmentId, so nothing can even name an attachment, let alone fetch/messages/{id}/attachments/{id}. Closing this needs no new OAuth scope —gmail.readonlyalready covers attachment download.2. There is no send path at all.
grep -rn -i "gmail.send\|gmail.compose\|messages/send\|mailchannels\|sendgrid\|resend\.\|smtp\|nodemailer" workers/api/src packages/*/srcreturns zero hits outside one unrelated word ("resend" in a CLI prompt string,lib/connector-tool-prompt.ts:218). PAGS cannot send an email by any route. This one does need a new scope (gmail.send) and therefore a re-consent path for anyone already connected.3. There is no PDF write path. The only PDF code is
lib/resume-parse.ts, which sends a PDF to Claude as adocumentcontent block to READ it (resume-parse.ts:65). Nopdf-lib, no form-field filling, nothing that produces a PDF.Shape of the work
Four issues, in dependency order. Each is independently useful — (1) alone makes "what did that email say, and what was attached" answerable.
gmail_search,gmail_read_message,gmail_download_attachmentfill_pdf_form— AcroForm fill, with an honest fallback when the PDF is flatgmail_reply/gmail_sendwith attachments,gmail.sendscope, write consent, re-consent migrationDesign decisions taken up front
permissions.emailstays a hard precondition for every Gmail tool, new ones included — not justfind_confirmation_link. Otherwise an agent that declaresgmail_searchincapabilities.toolsgains mailbox reach without the owner's existing toggle, which is a silent widening of an owner-facing control that exists today.GOOGLE_SHEETS_MANIFEST(lib/connectors/google-sheets.ts:56) — declarativeparams+ handlers compiled bycompileConnector. This supersedes thetools: []rationale in theconnected-accounts.tsheader, which argued the registry had no grant model forfind_confirmation_link's permission-flag gate. That argument is about that tool and stays true; it is not a reason for the connector to carry no tools at all. The header comment gets updated rather than left to contradict the code.scopes.writemust flip totruefor send to be reachable at all:assertScoperefuses a write-scoped tool on a connector declaringwrite:false(lib/connectors/client.ts:84-87). That flip is what makes the [connectors] Connector consent + write-scope safety + admin visibility #90 write-consent gate applicable, so send is consent-gated rather than ungated.gmail.sendis sufficient to reply in-thread (passthreadIdplusIn-Reply-To/Referencesheaders);gmail.modifyis not needed and is not requested.Not verified / open