molecule-ai

gmail-channel-molecule (0.1.3)

Published 2026-07-28 21:50:02 +00:00 by pypi-publisher

Installation

pip install --index-url https://git.moleculesai.app/api/packages/molecule-ai/pypi/simple/ --extra-index-url https://pypi.org/simple gmail-channel-molecule

About this package

Gmail inbound channel bridge for a Molecule workspace (poll-mode A2A relay, stdlib-only, per-workspace OAuth).

gmail-channel-molecule

First-party Gmail inbound channel for Molecule workspaces. Polls a Gmail mailbox (stdlib only) and relays each new message into a workspace over A2A message/send, replay-guarded by a durable poll cursor. Credentials are per-workspace — minted via a Google device grant (gmail-connect skill) and stored as the workspace's own secrets; nothing mailbox-specific is baked in.

Modeled on plugins/lark-channel. Discoverable + install-on-demand via the molecule-core manifest.json plugin catalog (NOT auto-installed).

Layout

Path Purpose
plugin.yaml Marketplace manifest — kind: channel, contributes the gmail-bridge daemon + gmail-connect skill.
gmail_channel_molecule/bridge.py Pure decision helpers (plan_message/to_envelope) + Gmail REST + A2A relay + poll loop.
gmail_channel_molecule/daemon.py Console entry — poll/daemon modes, env-wins credential loading, connect dispatch.
gmail_channel_molecule/onboard.py Google device-grant onboarding → writes GMAIL_* workspace secrets + restart.
skills/gmail-connect/SKILL.md Agent-driven connect flow (start → status → finalize).
daemon-bootstrap.sh contributes.daemons entry — installs delivered source, execs the daemon.

Connect

See skills/gmail-connect/SKILL.md. In short: create a Google OAuth client of type "TV and Limited Input devices", export GMAIL_CLIENT_ID/GMAIL_CLIENT_SECRET, then gmail-channel-molecule connect start | status | finalize.

Scope (v0.1.0)

  • Inbound only (gmail.readonly). Replies-to-email would be a v2 adding gmail.send.
  • The daemon runs from delivered source (no published wheel required). The connect onboarding CLI is pip installed on demand; the wheel is published to the org Gitea PyPI registry by auto-releasepublish (a green merge to main cuts tag v<version>, which builds + uploads the wheel), so pip install gmail-channel-molecule==0.1.0 resolves.
  • Unit-tested (pure helpers + onboarding finalize contract); a live OAuth + relay verification against a real mailbox is recommended before fleet rollout.

Diagnosing a silent channel

The state dir (GMAIL_CHANNEL_STATE_DIR, else MOLECULE_PLUGIN_STATE_DIR — the platform-assigned, already-namespaced dir from the plugin-state contract — else ~/.gmail-channel) holds two files:

File Written Read it to answer
cursor.json only when mail moves where the high-water-mark is; which messages are deferred
status.json every tick, always is the loop still ticking, what did the last tick fail on, and is state durable

status.json reports the plugin-state posture. "state_durable": false means the cursor does not survive a container recreate; "degraded" then carries the operator-facing explanation and "cursor_seed_mode" says how this boot seeded the high-water-mark ("pinned-now" when durable, "bounded-lookback" when not). Read those before concluding a channel is healthy.

cursor.json alone cannot distinguish "the daemon is wedged" from "the mailbox is quiet" — both look like a frozen mtime. status.json carries tick_finished_at and last_error, so a stale beat means wedged and a fresh beat with a last_error names the failure.

Poll-loop invariants worth knowing before debugging:

  • A tick cannot run forever: GMAIL_TICK_BUDGET_SECONDS (default 300) is a hard SIGALRM deadline. urlopen's timeout= is a per-socket-operation timeout, not a deadline, so it is not by itself a bound on a tick.
  • A single message can never wedge the channel: one that Gmail won't hand over is skipped, one whose sender hasn't materialized is deferred (MAX_DEFER ticks, then promoted with a fallback sender), and one the platform refuses is retried next tick. In all three cases newer mail keeps flowing.
  • The cursor is a commit frontier: it never advances past a message that was not relayed. Messages relayed ahead of the frontier are held in _promoted until it catches up, which is what makes flowing past a stuck message safe from duplicates.

State durability, and the trade when there is none

The manifest declares contributes.state: {durability: required} (plugin-state contract, RFC molecule-ai-sdk#181). The runtime answers with a directory and an honest MOLECULE_PLUGIN_STATE_DURABLE flag; the plugin never guesses either.

When durable the first-run branch pins the high-water-mark to now() — it fires once, ever, and suppresses a one-off backlog flood.

When not durable the cursor is wiped on every container recreate. Pinning to now() then re-pins the mark on every restart, making all older mail permanently invisible — that is runtime#360 defect A, measured live at 18:53:11Z on one deploy and 20:51:52Z on the next. So the bridge instead re-seeds to a bounded lookback (the query's own newer_than: window; GMAIL_NONDURABLE_LOOKBACK_SECONDS overrides, capped at 7d).

That is a deliberate trade, and it is not free. Replay suppression in this plugin is entirely local (the cursor frontier + _promoted), and both die with the state dir. x_sync_uuid is a stable id, not a dedup guarantee: nothing on the platform reads it, and the nearest mechanism — the a2a_queue idempotency index on the JSON-RPC messageId — is partial (WHERE status IN ('queued','dispatched')), so it only collapses a re-POST of a turn still in flight. A completed turn re-enqueues freely.

So on a non-durable substrate, mail inside the lookback window can be delivered twice. That was chosen over the alternative because a re-delivered email is visible and recoverable while a silently dropped one is neither. An operator who would rather miss mail than ever double-deliver it sets GMAIL_NONDURABLE_LOOKBACK_SECONDS=0, which restores the old pin-to-now() behaviour.

The durable fix is for the provisioner to supply a real MOLECULE_PLUGIN_STATE_ROOT for the workspace; then none of the above applies.

Requirements

Requires Python: >=3.11
Details
PyPI
2026-07-28 21:50:02 +00:00
11
Molecule AI
Proprietary
62 KiB
Assets (2)
Versions (11) View all
0.3.3 2026-08-10
0.3.2 2026-08-08
0.3.1 2026-08-08
0.3.0 2026-08-08
0.2.0 2026-08-04