gmail-tools — Operations Reference

Repo: radieu/gmail-tools (private) — created 2026-07-30, main-only branch model (no dev) Origin issue: #2715 Scaffolding tracked by: #3471 Ecosystem onboarding tracked by: #4721 (SOPS + Supabase + bms-4 deploy, in progress) Host: bms-4 (/opt/gmail-tools) · Bot: n8n workflow google-worker-bot


Purpose

gmail-tools is a Node.js CLI for managing Google APIs (Gmail, Calendar, Sheets, Drive, Docs, YouTube) via a service account + OAuth. Dev machine path: C:\code_2026\gmail-tools\ (this is the canonical path — d:\code_2026\gmail-tools\ referenced in older issues is the same checkout via a drive-letter symlink, not a separate divergent copy). The goal is: a private GitHub repo (done), a clone on bms-4, and an n8n workflow (google-worker-bot) that exposes it as a Telegram bot.


Component map

ComponentWhereStatus
GitHub repo radieu/gmail-toolsGitHubDone — created 2026-07-30, private, main-only. Standard p24-infra labels applied. AI-Dev-BMS4-1 + ai-dev-bms4-2 collaborators accepted, push=true verified 2026-07-31 (#4721)
.claude/ skills + settingsC:\code_2026\gmail-tools\.claude\Done — sr, commit, cleanup-branches, fix-fast, create-pr (adapted for main-only)
Source (auth.js, calendar-cli.js, package.json, docs/)dev machine C:\code_2026\gmail-tools\Push comes from the owner / an SSH-capable worker
Secrets → SOPS+agep24-infra/secrets/gmail-tools.env.sopsDone — 7 keys, covered by the .sops.yaml catch-all rule, canary-verified 2026-07-31 (#4721)
Clone /opt/gmail-tools + npm installbms-4Done — cloned, deps installed, credentials/ (0700) populated from SOPS and digest-verified 2026-07-31 (#4721)
n8n workflow google-worker-bot (id BBVD0cAN6uuIbcqr)n8n on bms-4✅ ACTIVE since 2026-08-11 (#2715). The 2026-07-04 prototype this row used to warn about (no-op allowedChatId=-1 auth guard + shell-injecting SSH nodes) was replaced on 2026-08-01 by a hardened rebuild: real owner-only auth guard (from.id == 8155641922), base64-transported prompt passed as a single claude -p argv, --allowedTools allowlist. That rebuild is AI-registered and security-assessed — its authoritative ops doc is gmail-tools-telegram-bot-operations.md. The old RCE warning in §[B] below is superseded and describes only the retired prototype.
n8n workflow gmail-tools — Daily Inbox Agent (id gli6YpO4t9e2koMc)n8n on bms-4Built, scheduled, INACTIVE — daily 07:00 Europe/Warsaw claude -p inbox review (#4724). Cron path verified; ships deactivated pending #4785 (SSH credential), radieu/gmail-tools#2 (expired OAuth grant) and an owner decision on the injection blast radius (C-R6(b)). Registered in dev_r_ai_systems (minimal EU AI Act tier). Ops: gmail-tools-daily-agent-operations.md
gmail-session-manager microservice (port 9998) + telegram-gmail-bot n8n workflowbms-4, own OS user gmail-runnerWaves 1-3, 5 done; wave 6 (n8n workflow) built INACTIVE 2026-08-04. gmail-runner user, GMAIL_SESSION_MANAGER_KEY + TELEGRAM_GMAIL_BOT_API_KEY in SOPS, isolated clone + npm ci, /var/lib/gmail-telegram/* state dirs, gmail-session-manager.service installed but not started, iptables rule for 9998, n8n workflow telegram-gmail-bot (id YjtA6n33pWvpReIo) built and verified inactive. Shared OAuth client (gcp-oauth.keys.json) staged into gmail-runner’s creds dir. Wave 4 still BLOCKED on the repo owner: gmail-runner needs its own interactive Google OAuth consent grant (token.json — not shared with claude-runner) before the service can start or the workflow can activate. Fully isolated from google-worker-bot below — different OS user, port, secret, and tool-permission model. Ops: telegram-gmail-bot-operations.md
Telegram botBotFatherHuman — no API substitute

Provisioning status

Repo creation is done (2026-07-30, from the gmail-tools dev-machine session — git remote was already pointed at radieu/gmail-tools and main tracked origin/main, so no separate repo-admin credential step was needed this time). The note below about the scaffolding worker’s permission failure is kept for history — it applied to an earlier automated attempt (#3471), not to how the repo actually got created.

[A] GitHub repo — human / repo-admin step

  1. Create private repo radieu/gmail-tools (no auto-README — the init commit comes from the dev machine).
  2. From C:\code_2026\gmail-tools\, push these to main: auth.js, calendar-cli.js, package.json, package-lock.json, .gitignore, docs/service-account-setup.md, docs/.
  3. Never commit: credentials/, node_modules/, *.tmp.js, gcp-oauth.keys.json (ensure .gitignore covers these).
gh repo create radieu/gmail-tools --private \
  --description "Node.js CLI for Google APIs via service account + OAuth"

[C] Clone on bms-4 — infra-task (SSH), after [A]

# as root on bms-4
install -d -o claude-runner -g claude-runner /opt/gmail-tools
su -s /bin/bash claude-runner -c '
  git clone https://github.com/radieu/gmail-tools.git /opt/gmail-tools
  cd /opt/gmail-tools && npm install
  install -d -m 700 /opt/gmail-tools/credentials
'

credentials/ is populated by decrypting secrets/gmail-tools.env.sops — SOPS is the source of truth, superseding the earlier “owner copies token.json + sa-key.json here” note.

Canonical placement — use the script, not an ad-hoc command (#4771):

# as claude-runner on bms-4, from a fresh /opt/p24-infra checkout
scripts/deploy-gmail-tools-credentials.sh
scripts/deploy-gmail-tools-credentials.sh --check   # verify; writes nothing

scripts/deploy-gmail-tools-credentials.sh decrypts the SOPS file, writes each credential file verbatim (printf '%s', never printf '%b' / echo -e), gates every file behind jq empty, and aborts before overwriting a good file with a corrupt one. It is idempotent and re-runnable. Do not hand-copy these values through a JSON round-trip or an unescape step — that is exactly what broke calendar-sa-key.json in the 4770 incident (see the warning below).

--check is the read-only counterpart and the sanctioned way to answer “is the deployment still healthy?” (#4721). It re-derives all four files from SOPS and reports each as OK / MISSING / INVALID / MISMATCH, exiting non-zero if any is not OK. It never creates or writes to DEST_DIR, tolerates formatting-only differences (jq -S equality), and reports every defective file rather than only the first. An escape-expanded calendar-sa-key.json surfaces as INVALID — the exact symptom that took the CLI down. Values are never printed in either mode; all diagnostics go to stderr, so they stay visible even when a helper runs inside $(…).

Key extraction goes through scripts/lib/extract_env_key.sh, which is anchored to ^KEY= and fails on a duplicate key instead of taking the first match — see #3494 for why a head -1 there is a credential-exposure risk, not a convenience.

Credential fileSOPS key
credentials/token.jsonGMAIL_TOOLS_OAUTH_TOKEN
credentials/calendar-sa-key.jsonGMAIL_TOOLS_CALENDAR_SA_KEY
credentials/sheets-config.jsonGMAIL_TOOLS_SHEETS_CONFIG
credentials/gcp-oauth.keys.jsonbuilt from GMAIL_TOOLS_GCP_OAUTH_CLIENT_ID + GMAIL_TOOLS_GCP_OAUTH_CLIENT_SECRET (shape: {"installed":{client_id,client_secret,redirect_uris,auth_uri,token_uri}})

⚠️ Write each value verbatim — never expand \n escapes (#4721, root-caused in #4771). GMAIL_TOOLS_CALENDAR_SA_KEY is a service-account key whose private_key field contains literal \n escape sequences that must stay escaped inside the JSON string. A placement step that ran the value through an unescape transform (printf '%b', echo -e, $'…', or a jq -r / ConvertFrom-Json → ConvertTo-Json round-trip that re-emits real newlines) turned the \n escapes into raw newline bytes, producing a calendar-sa-key.json that jq empty / JSON.parse reject with Bad control character in string literal (~position 164) — and calendar-cli.js dies before making any API call. The stored SOPS value is already valid, minified, single-line JSON — the #4770 SOPS “fix” was a confirmed no-op because the secret was never corrupt; only the extraction/write step was. Write it byte-for-byte (a single trailing newline is fine) and do not transform it. The script above enforces this; scripts/tests/test_deploy_gmail_tools_credentials.py is the regression guard.

A genuine rewrite of GMAIL_TOOLS_CALENDAR_SA_KEY (e.g. rotating the real key) is a secret-manager operation on the dev workstation — the source value lives only at C:\code_2026\gmail-tools\credentials\calendar-sa-key.json. A Linux queue worker cannot do it.

Verify after populating:

su -s /bin/bash claude-runner -c '
  /opt/p24-infra/scripts/deploy-gmail-tools-credentials.sh --check
  node -e "require(\"/opt/gmail-tools/credentials/calendar-sa-key.json\"); console.log(\"parses OK\")"
  cd /opt/gmail-tools && GOOGLE_SERVICE_ACCOUNT_PATH=/opt/gmail-tools/credentials/calendar-sa-key.json \
    GCAL_DEFAULT_CAL=radieu@gmail.com node calendar-cli.js calendars
'

An empty [] from calendars means auth succeeded but no calendar has been shared with the service-account email yet — that share is a Google-account step only the owner can perform.

[B] n8n workflow — google-worker-bot

⚠️ SUPERSEDED (2026-08-11, #2715) — this warning applies only to the RETIRED 2026-07-04 prototype, which was replaced on 2026-08-01 and no longer exists in n8n. The live BBVD0cAN6uuIbcqr is the hardened rebuild (real owner-only auth guard, base64→argv prompt, --allowedTools) and is active. For its current design and operations use the authoritative ops doc gmail-tools-telegram-bot-operations.md. The two flaws described below were real in the 2026-07-04 draft (which never ran — 0 executions) and are both fixed in the rebuild; the text is kept only as a record of what was remediated.

A draft of this workflow was originally built in n8n on bms-4 (2026-07-04) but left inactive (verified: active=false, 0 executions — never activated, never triggered, so no exploit occurred). That draft’s node code had two flaws that would have made it a remote-code-execution vector if activated:

  1. Auth Guard is a no-op. The guard reads const allowedChatId = -1 and only rejects when allowedChatId !== -1, so the placeholder -1 short-circuits the check and every Telegram sender passes. The real owner message.from.id must gate the flow before it can be activated.
  2. Shell injection. The Exec: * SSH nodes interpolate raw Telegram text (node ./calendar-cli.js {{ $json.action }} {{ $json.params }}) directly into a shell command with no escaping. Combined with (1), any sender could execute commands on bms-4.

Do not activate this draft. The rebuild below must set a real allowedChatId and pass action/params as argv (never string-interpolated into the shell). This vector is being replaced by a properly-scoped redesign in radieu/gmail-tools.

Build in n8n on bms-4 once [A]+[C] and the bot token are ready. Flow:

  1. Telegram Trigger (polling) — credential Telegram Bot — gmail-worker (token from BotFather).
  2. Auth guard (Code) — accept only the owner’s message.from.id.
  3. Command parser (Code) — map text → { service, action, params }:
    • /cal list | kalendarzcalendar:list
    • /cal add <title> <ISO-datetime>calendar:add
    • /drive listdrive:list
    • /sheet <ID>sheets:read
  4. Router — Switch on service.
  5. Execute Command (one per service), e.g. calendar: node /opt/gmail-tools/calendar-cli.js {{action}} {{params}}
    • calendar env: GOOGLE_SERVICE_ACCOUNT_PATH=/opt/gmail-tools/credentials/calendar-sa-key.json, GCAL_DEFAULT_CAL=radieu@gmail.com
    • OAuth services (drive/gmail): GOOGLE_TOKEN_PATH=/opt/gmail-tools/credentials/token.json, GOOGLE_CREDS_PATH=/opt/gmail-tools/credentials/gcp-oauth.keys.json
  6. Response formatter (Code) — JSON → readable text.
  7. Telegram Send — reply to the same chat_id.

n8n credentials required: Telegram Bot — gmail-worker (BotFather token) + the credential file paths from [C].


Human / deferred tail (summary)

Everything below is what remains after the #4721 onboarding (repo, collaborators, SOPS file, bms-4 clone and credential population are all done):

  • Telegram bot — created via BotFather by the owner; token handed to n8n credentials separately. No API substitute exists.
  • Calendar sharing — the target calendar must be shared with the service-account client_email from calendar-sa-key.json. Until then calendar-cli.js calendars authenticates successfully but returns []. Owner-only Google-account step.
  • n8n workflow google-worker-bot — blocked on the Telegram bot token above.

Secrets

secrets/gmail-tools.env.sops is the source of truth (7 keys, covered by the .sops.yaml catch-all secrets/.*\.env\.sops$ rule — no per-file path_regex entry needed). The files under /opt/gmail-tools/credentials/ (chmod 700) are a derived deployment of those values, never the origin. The Telegram bot token lives in n8n credentials. Never commit any credential file — .gitignore must exclude credentials/ and gcp-oauth.keys.json.

QUEUE_API_KEY / QUEUE_API_URL are also in that SOPS file, so gmail-tools can dispatch its own [SECRET-REQUEST] / infra-task-request issues per docs/playbooks/secret-manager-request.md.


Compliance

  • gmail-tools is registered in Supabase dev_r_services (service_type=workflow, element_type=script, host=bms-4, criticality=low, compliance_workbook=yes). If AI-powered routing is added later, google-worker-bot needs its own row plus a dev_r_ai_systems entry.
  • This ops doc satisfies the “ops doc in docs/” requirement for the new repo.