gmail-tools — Operations Reference
Repo:
radieu/gmail-tools(private) — created 2026-07-30, main-only branch model (nodev) 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 workflowgoogle-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
| Component | Where | Status |
|---|---|---|
GitHub repo radieu/gmail-tools | GitHub | Done — 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 + settings | C:\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+age | p24-infra/secrets/gmail-tools.env.sops | Done — 7 keys, covered by the .sops.yaml catch-all rule, canary-verified 2026-07-31 (#4721) |
Clone /opt/gmail-tools + npm install | bms-4 | Done — 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-4 | Built, 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 workflow | bms-4, own OS user gmail-runner | Waves 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 bot | BotFather | Human — 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
- Create private repo
radieu/gmail-tools(no auto-README — the init commit comes from the dev machine). - From
C:\code_2026\gmail-tools\, push these tomain:auth.js,calendar-cli.js,package.json,package-lock.json,.gitignore,docs/service-account-setup.md,docs/. - Never commit:
credentials/,node_modules/,*.tmp.js,gcp-oauth.keys.json(ensure.gitignorecovers 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 nothingscripts/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 file | SOPS key |
|---|---|
credentials/token.json | GMAIL_TOOLS_OAUTH_TOKEN |
credentials/calendar-sa-key.json | GMAIL_TOOLS_CALENDAR_SA_KEY |
credentials/sheets-config.json | GMAIL_TOOLS_SHEETS_CONFIG |
credentials/gcp-oauth.keys.json | built 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
\nescapes (#4721, root-caused in #4771).GMAIL_TOOLS_CALENDAR_SA_KEYis a service-account key whoseprivate_keyfield contains literal\nescape 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 ajq -r/ConvertFrom-Json → ConvertTo-Jsonround-trip that re-emits real newlines) turned the\nescapes into raw newline bytes, producing acalendar-sa-key.jsonthatjq empty/JSON.parsereject withBad control character in string literal(~position 164) — andcalendar-cli.jsdies 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.pyis the regression guard.A genuine rewrite of
GMAIL_TOOLS_CALENDAR_SA_KEY(e.g. rotating the real key) is asecret-manageroperation on the dev workstation — the source value lives only atC:\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
BBVD0cAN6uuIbcqris 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 docgmail-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:
- Auth Guard is a no-op. The guard reads
const allowedChatId = -1and only rejects whenallowedChatId !== -1, so the placeholder-1short-circuits the check and every Telegram sender passes. The real ownermessage.from.idmust gate the flow before it can be activated.- 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
allowedChatIdand passaction/paramsas argv (never string-interpolated into the shell). This vector is being replaced by a properly-scoped redesign inradieu/gmail-tools.
Build in n8n on bms-4 once [A]+[C] and the bot token are ready. Flow:
- Telegram Trigger (polling) — credential
Telegram Bot — gmail-worker(token from BotFather). - Auth guard (Code) — accept only the owner’s
message.from.id. - Command parser (Code) — map text →
{ service, action, params }:/cal list|kalendarz→calendar:list/cal add <title> <ISO-datetime>→calendar:add/drive list→drive:list/sheet <ID>→sheets:read
- Router — Switch on
service. - 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
- calendar env:
- Response formatter (Code) — JSON → readable text.
- 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_emailfromcalendar-sa-key.json. Until thencalendar-cli.js calendarsauthenticates 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-toolsis registered in Supabasedev_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-botneeds its own row plus adev_r_ai_systemsentry.- This ops doc satisfies the “ops doc in
docs/” requirement for the new repo.