Part of the GSuite tool integrations epic. Depends on the shared resolver issue (core/google_access.py).
Problem Statement
Agents cannot read, search, or act on Gmail. We need a tool_gmail node exposing the full Gmail surface so agents can triage, label, draft, and send mail, with destructive operations gated.
Proposed Solution
Build nodes/src/nodes/tool_gmail/ following the nodes/src/nodes/tool_github/ reference pattern (anatomy below). Build on the Gmail API v1 via google-api-python-client.
Node anatomy
__init__.py, services.json, IGlobal.py, IInstance.py, gmail_client.py, requirements.txt, README.md, and nodes/test/tool_gmail/test_*.py.
IGlobal.beginGlobal() reads Config.getNodeConfig(...), calls resolve_google_access(cfg, GMAIL) from nodes.core.google_access, builds the Gmail client. Skips when openMode == OPEN_MODE.CONFIG.
IInstance exposes one @tool_function per operation, using normalize_tool_input, require_str, require_int, and gating via the resolved access object.
- Deps:
google-api-python-client, google-auth, google-auth-oauthlib, google-auth-httplib2. Installed via depends(), skipped under ROCKETRIDE_MOCK. SDK mocked under nodes/test/mocks/google/....
Auth (document both, implementer picks)
Merge the google.* block from nodes/src/nodes/core/services.common.google.json. User OAuth (oAuthButton plus userToken, code exchange via packages/ai/src/ai/web/endpoints/auth_callback.py) is the natural fit for per-user mailboxes. Service account with domain-wide delegation (serviceKey, adminEmail) suits org automation.
Full tool surface (no omissions)
Reads and organization (Tier 1, available at every tier that grants the scope):
message_list, message_get, message_search (Gmail query syntax), message_modify, message_batch_modify (add or remove labels including UNREAD for read-state), thread_get, thread_list, label_list, label_apply, label_remove, label_create, label_update, label_delete, draft_list, draft_get, attachment_get, history_list (incremental sync via historyId).
Write tier (Tier 2): message_send (must support reply-in-thread: accept threadId and set In-Reply-To and References headers, or replies post as new threads), message_trash, message_untrash, draft_create, draft_update, draft_send, draft_delete.
Gated by allowHardDelete (Tier 3, off by default): message_delete, messages_batchDelete. Batch delete takes an explicit message-ID list, capped per call by an in-code constant, never a query.
Access enum and scopes
access: readonly, modify, send. Resolved by the GMAIL spec in core/google_access.py (readonly -> gmail.readonly; modify -> gmail.modify, which covers label CRUD; send -> gmail.modify plus gmail.send). Never hand-enter scope strings.
Acceptance Criteria / Definition of Done
- Every function above is implemented as a registered
@tool_function. Gated functions register and return a clear GoogleAccessError message when their gate is off, not a missing function.
- Per function, the issue-time spec is honored: input schema lists required vs optional fields with defaults; output is a cleaned shape (ids, labelIds, snippet, headers, threadId, historyId as relevant), not raw API JSON; operational targets (messageId, threadId, labelId, query) are invoke-time params, never config.
message_send reply correctness: when threadId is supplied, In-Reply-To and References are set and the message lands in the existing thread (test asserts this).
message_batch_modify and messages_batchDelete accept explicit ID lists only and enforce the per-call cap.
- Read-state changes go through label add/remove of UNREAD via message_modify / message_batch_modify.
- Tests: success and error path per function, plus a mock run under
ROCKETRIDE_MOCK. A test asserts every GMAIL spec flag name (allowHardDelete) exists as a services.json field.
Config panel
Merges the shared Google auth block, plus:
"fields": [
{ "name": "access", "type": "select", "default": "modify",
"options": ["readonly", "modify", "send"], "label": "Access level" },
{ "name": "allowHardDelete", "type": "boolean", "default": false,
"label": "Allow permanent delete (bypasses Trash, irreversible)" }
]
Alternatives Considered
Single combined tool_gsuite node exposing every app's tools at once: rejected. Per-app nodes keep the agent tool surface small and let access and gates be set per app.
Affected Modules
Part of #1053.
Part of the GSuite tool integrations epic. Depends on the shared resolver issue (core/google_access.py).
Problem Statement
Agents cannot read, search, or act on Gmail. We need a
tool_gmailnode exposing the full Gmail surface so agents can triage, label, draft, and send mail, with destructive operations gated.Proposed Solution
Build
nodes/src/nodes/tool_gmail/following thenodes/src/nodes/tool_github/reference pattern (anatomy below). Build on the Gmail API v1 viagoogle-api-python-client.Node anatomy
__init__.py,services.json,IGlobal.py,IInstance.py,gmail_client.py,requirements.txt,README.md, andnodes/test/tool_gmail/test_*.py.IGlobal.beginGlobal()readsConfig.getNodeConfig(...), callsresolve_google_access(cfg, GMAIL)fromnodes.core.google_access, builds the Gmail client. Skips whenopenMode == OPEN_MODE.CONFIG.IInstanceexposes one@tool_functionper operation, usingnormalize_tool_input,require_str,require_int, and gating via the resolved access object.google-api-python-client,google-auth,google-auth-oauthlib,google-auth-httplib2. Installed viadepends(), skipped underROCKETRIDE_MOCK. SDK mocked undernodes/test/mocks/google/....Auth (document both, implementer picks)
Merge the
google.*block fromnodes/src/nodes/core/services.common.google.json. User OAuth (oAuthButton plus userToken, code exchange viapackages/ai/src/ai/web/endpoints/auth_callback.py) is the natural fit for per-user mailboxes. Service account with domain-wide delegation (serviceKey, adminEmail) suits org automation.Full tool surface (no omissions)
Reads and organization (Tier 1, available at every tier that grants the scope):
message_list, message_get, message_search (Gmail query syntax), message_modify, message_batch_modify (add or remove labels including UNREAD for read-state), thread_get, thread_list, label_list, label_apply, label_remove, label_create, label_update, label_delete, draft_list, draft_get, attachment_get, history_list (incremental sync via historyId).
Write tier (Tier 2): message_send (must support reply-in-thread: accept threadId and set In-Reply-To and References headers, or replies post as new threads), message_trash, message_untrash, draft_create, draft_update, draft_send, draft_delete.
Gated by
allowHardDelete(Tier 3, off by default): message_delete, messages_batchDelete. Batch delete takes an explicit message-ID list, capped per call by an in-code constant, never a query.Access enum and scopes
access: readonly, modify, send. Resolved by the GMAIL spec incore/google_access.py(readonly -> gmail.readonly; modify -> gmail.modify, which covers label CRUD; send -> gmail.modify plus gmail.send). Never hand-enter scope strings.Acceptance Criteria / Definition of Done
@tool_function. Gated functions register and return a clearGoogleAccessErrormessage when their gate is off, not a missing function.message_sendreply correctness: when threadId is supplied, In-Reply-To and References are set and the message lands in the existing thread (test asserts this).message_batch_modifyandmessages_batchDeleteaccept explicit ID lists only and enforce the per-call cap.ROCKETRIDE_MOCK. A test asserts every GMAIL spec flag name (allowHardDelete) exists as aservices.jsonfield.Config panel
Merges the shared Google auth block, plus:
Alternatives Considered
Single combined
tool_gsuitenode exposing every app's tools at once: rejected. Per-app nodes keep the agent tool surface small and let access and gates be set per app.Affected Modules
Part of #1053.