Skip to content

feat(nodes): tool_gmail — Google Gmail tool node #1055

@joshuadarron

Description

@joshuadarron

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

  • nodes (pipeline)
  • ai

Part of #1053.

Metadata

Metadata

Assignees

Labels

P2-mediumMedium priorityaiAI/ML modulesfeatureNew feature or enhancementnodesPipeline nodes

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions