Skip to content

WhatsApp β†’ Hermes Admin Control Channel ​

Runbook for letting a trusted phone issue admin/system commands to Hermes over WhatsApp (the bridge's "Admin Mode"). The infra is already built; turning it on for a number is config only β€” no code changes.

Canonical source and deployment ownership live in this Flutter repository: services/hermes-server/ and supabase/functions/, backed by Supabase jrsgosnnyjonxaesqtln. React is only the immutable rollback client during the compatibility window.

⚠ Status check before you follow anything below (verified 2026-09-22) ​

The WhatsApp admin line itself is live β€” that part of this runbook is current. HERMES_WHATSAPP_ADMIN_APPROVALS_ENABLED, HERMES_WHATSAPP_AUTO_LOW_RISK and HERMES_WHATSAPP_TASK_DISPATCH_ENABLED are all true in production, so the "default off" notes below are stale.

The crm_* MCP surface this document describes β€” the "81 tools", the crm_propose_* wrappers, the enable sequences that say "restart both gateways" and "update gateway SOUL.md" β€” reached its consumer through the Nous agent gateway, which was RETIRED on 2026-07-11 at the Gemini-only cutover (src/config.ts: "its original consumer … was retired"). The MCP servers are still mounted (HERMES_MCP_ENABLED and HERMES_PUBLIC_MCP_ENABLED are true on the box) but nothing consumes them.

So: the admin capabilities below are real and reachable from WhatsApp; the MCP transport and the gateway restart steps are not. Do not use the counts or the deploy sequences as a description of what is wired today β€” read GET /api/admin/capabilities, which serialises the live registries.

How it works ​

sender (allowlisted phone)
  └─ WhatsApp message ─▢ business number (e.g. 96522466862)
       └─ Meta Cloud API webhook ─▢ edge fn  whatsapp-webhook
            getEntityConfig(phone_number_id) β†’ entity            (env: WHATSAPP_PHONE_NUMBER_ID_*)
            └─ whatsapp_ai_config[entity].ai_engine == 'hermes'  β†’ whatsapp-webhook-async
                 └─ edge fn  whatsapp-hermes-bridge
                      sender ∈ HERMES_WHATSAPP_ADMIN_PHONES ?     (env, comma-sep digits, no +)
                        yes β†’ Admin Mode: OTP β†’ admin session β†’ classifyAdminCommand
                               └─ Hermes agent on the private VPS (HERMES_SERVER_URL) executes
                        no  β†’ normal AI assistant reply
  • Allowlist is matched against the sender (normalizePhone strips +, so +965… matches 965…). Empty/unset HERMES_WHATSAPP_ADMIN_PHONES = Admin Mode disabled.
  • Two conditions, not one. The allowlist says WHO; ADMIN_ENTITY says WHERE. An allowlisted phone texting a customer line is a customer β€” the public aldilaijan number never grants staff tier. Enforced twice: edge (adminLineActive, whatsapp-hermes-bridge/handler.ts) and server (HERMES_WHATSAPP_ADMIN_ENTITY, routes/whatsappBridge.ts). Keep both values equal.
  • Entity↔number mapping is by Meta phone_number_id, resolved in whatsapp-webhook's getEntityConfig() from the env slots WHATSAPP_PHONE_NUMBER_ID_{PERSONAL,ALDILAIJAN,KHOBARA,ALDILAIJAN_EXEC,CLIENT,ADMIN} (+ matching WHATSAPP_ACCESS_TOKEN_*).

Current admin setup (as configured) ​

  • Sender (admin): +96562224000 (ΨΉΨ¨Ψ―Ψ§Ω„Ψ±Ψ­Ω…Ω† β€” super admin).
  • Control number: +96522466862 (staff command line).
  • Entity: admin.
  • Meta phone_number_id for 96522466862: 1022182970975277.

⚠️ Retired entities: aldilaijan_exec and client. Neither resolves to a line. aldilaijan_exec has no WHATSAPP_PHONE_NUMBER_ID_* secret at all, and WHATSAPP_PHONE_NUMBER_ID_CLIENT held the same Meta id as _ALDILAIJAN, which getEntityConfig() checks first. ADMIN_ENTITY was left on aldilaijan_exec after the July admin cutover, so Admin Mode silently never engaged and +…862 answered like a customer line. If you ever see the staff line reply with the customer transfer message, check ADMIN_ENTITY first.

Done on the system side ​

  • whatsapp_ai_config entity admin β†’ ai_engine = 'hermes', is_active = true.
  • Admin-Mode persistence exists: whatsapp_admin_sessions, whatsapp_admin_otp_log, whatsapp_admin_audit.

Supabase Edge-Function secrets ​

(Dashboard β†’ project jrsgosnnyjonxaesqtln β†’ Edge Functions β†’ Secrets, or supabase secrets set NAME="…" --project-ref jrsgosnnyjonxaesqtln)

SecretValue
WHATSAPP_PHONE_NUMBER_ID_ADMIN1022182970975277
WHATSAPP_ACCESS_TOKEN_ADMINSystem-User token (falls back to _ALDILAIJAN)
ADMIN_ENTITYadmin β€” must name an entity with a phone-number-id secret
HERMES_WHATSAPP_ADMIN_PHONES96562224000 (comma-separate to add more)

Secret values are stored as sha256(value), so you can verify one without reading it: python -c "import hashlib;print(hashlib.sha256('admin'.encode()).hexdigest())" and compare against supabase secrets list.

Meta onboarding (external β€” done in Meta WhatsApp Cloud API) ​

Standard path (API-only number) ​

  1. Add the number to the WhatsApp Business Account and verify it. ⚠️ The number must be clean β€” not active on the consumer WhatsApp or Business app.
  2. Copy its phone_number_id (e.g. 1022182970975277 for exec).
  3. Subscribe the webhook:
    • URL https://jrsgosnnyjonxaesqtln.supabase.co/functions/v1/whatsapp-webhook
    • Verify token = existing WHATSAPP_WEBHOOK_VERIFY_TOKEN
    • Subscribe the messages field (and status fields you use).
  4. Generate the permanent access token β†’ put it in the secret above.

Coexistence path (keep WhatsApp Business app on the same number) ​

Use this when the number must stay on WhatsApp Business for Android while the CRM inbox also works.

  1. Follow WHATSAPP_COEXISTENCE.md β€” Embedded Signup β†’ Connect your existing WhatsApp Business app, QR scan, secrets, deploy.
  2. Subscribe webhook fields messages and smb_message_echoes (phone-sent replies mirror into CRM).
  3. Verify with Graph API: is_on_biz_app=true and platform_type=CLOUD_API.

Do not use the β€œclean number” standard path if you intend to keep the Business app active β€” use Coexistence instead.

Dependency ​

Command execution runs on the private Hermes VPS (HERMES_SERVER_URL). Admin-Mode commands must be enabled there too (see HERMES_AI_GATEWAY.md / the hermes-server repo).

Verify (after secrets + subscription + a test message) ​

Send a WhatsApp from 96562224000 β†’ 96522466862, then run:

sql
-- 1) inbound resolved to the right entity (proves the phone_number_id secret)
select entity, phone_number_id, max(created_at)
from whatsapp_webhook_events
where phone_number_id = '1022182970975277' group by 1,2;          -- expect entity = admin

-- 1b) the agent actually ran (a non-null run id). A NULL hermes_run_id with
--     hermes_sent = true means the bridge answered from a canned branch β€”
--     the classic symptom of ADMIN_ENTITY not matching this entity.
select entity, engine, forced_engine, hermes_sent, hermes_run_id, created_at
from whatsapp_ai_dispatch_events order by created_at desc limit 5;

-- 2) Admin Mode engaged for the sender (proves HERMES_WHATSAPP_ADMIN_PHONES)
select * from whatsapp_admin_otp_log    order by created_at desc limit 5;
select * from whatsapp_admin_sessions   order by created_at desc limit 5;
select * from whatsapp_admin_audit      order by created_at desc limit 10;

If (1) is empty β†’ the number isn't reaching the webhook (subscription) or the phone_number_id secret is wrong. If (1) resolves but (2) is empty β†’ the sender isn't in HERMES_WHATSAPP_ADMIN_PHONES, or the bridge isn't reached (ai_engine not hermes).

Security ​

Anyone in HERMES_WHATSAPP_ADMIN_PHONES can open an OTP-gated admin session and run system commands. Keep it to trusted executives. Removing a number from the secret revokes it immediately.

References ​

  • Edge fns: whatsapp-webhook, whatsapp-webhook-async, whatsapp-hermes-bridge
  • Config: whatsapp_ai_config(entity, ai_engine); admin tables above
  • Env: HERMES_WHATSAPP_ADMIN_PHONES, ADMIN_ENTITY (edge) / HERMES_WHATSAPP_ADMIN_ENTITY (hermes-server) β€” keep equal, WHATSAPP_PHONE_NUMBER_ID_*, WHATSAPP_ACCESS_TOKEN_*, WHATSAPP_WEBHOOK_VERIFY_TOKEN, HERMES_SERVER_URL
  • Coexistence (phone + CRM): WHATSAPP_COEXISTENCE.md

Admin-line powers (Nous gateway path, shipped 2026-07-07) ​

Separate from the OTP admin mode above: the three admins allowlisted on the admin Nous line (hermes-server HERMES_WHATSAPP_ADMIN_PHONES β†’ Opus + admin CRM MCP) got three power upgrades (web repo PR busami/aldilaijanre#1007):

  1. Name search β€” crm_safe_query can find clients/deals/requests/tasks/ properties by name substring (no UUID needed).
  2. Approve/reject from chat β€” crm_list_pending_actions, crm_approve_action (approves AND executes), crm_reject_action on the agent_action_queue, including high-risk actions (exact title echo required). Reviews are attributed to the REAL admin: hermes-server resolves the sender phone (HERMES_WHATSAPP_ADMIN_USER_MAP, fallback profiles.phone + admin role) and injects a signed 15-min admin_session_token each turn; the tools derive the approver only from that token, and execution runs under a JWT minted for that admin (RLS). Rows approved this way show that admin in the Flutter/web Agent Action Inbox exactly like an inbox approval.
  3. Low-risk autonomy β€” with HERMES_WHATSAPP_AUTO_LOW_RISK, a verified admin's low-risk proposals (notes, statuses, follow-up tasks…) skip the inbox and auto-execute via the autonomous executor (all gates + caps apply); provenance in payload.whatsapp_admin.

Flags (HERMES_WHATSAPP_ADMIN_APPROVALS_ENABLED, HERMES_WHATSAPP_AUTO_LOW_RISK) default off β€” see the PR's deploy notes for the enable sequence (env β†’ redeploy β†’ restart both gateways β†’ seed hermes_autonomy_flags β†’ update gateway SOUL.md).

Round 2 β€” broadened write surface + task dispatch (shipped 2026-07-07) ​

A second web-repo PR extended the admin line further, all approval-gated:

  1. Full CRM write coverage β€” the granular crm_propose_* proposers grew from 10 to 44, covering finance (invoices, payments, quotations), contracts, the Khobara valuation workflow (visits, comparables), lifecycle creates (property/deal/request), assignments, owner-request triage, market/matching, archive, and email (compose β†’ approve β†’ send). Higher-risk ones stage into the Agent Action Inbox β€” nothing changes until you approve it. (Round 3 made the low-risk note/status/task wrappers auto-execute like their originals; see below.)
  2. Command Center task dispatch β€” crm_enqueue_task / crm_list_tasks / crm_get_task / crm_cancel_task let an admin start an autonomous background job from WhatsApp ("chase every overdue invoice and draft reminders"), check its progress, and stop it. A dispatched task runs on its own but stages every change through the approval queue β€” it never writes or sends without approval. Gated by HERMES_WHATSAPP_TASK_DISPATCH_ENABLED plus the runner's tasks.enabled and per-admin quota/budget.

Enable Round 2: set HERMES_WHATSAPP_TASK_DISPATCH_ENABLED=true (load with docker compose up -d hermes, not restart), restart both gateways, confirm tasks.enabled, and update the gateway SOUL.md. No migration needed.

Round 3 β€” polish (shipped 2026-07-08) ​

Two web-repo PRs (#1022, #1023):

  1. crm_followup_task (5th task tool) β€” continue a finished / failed / cancelled / awaiting-approval background task with a new instruction ("also check X", "redo it for Salmiya only"); it re-runs in the same thread. Admin MCP surface is now 81 tools.
  2. Consistent low-risk autonomy β€” the broadened crm_propose_* wrappers now follow each action's own risk tier, like the original ten: low-risk notes / status / task actions auto-execute for a verified admin (with HERMES_WHATSAPP_AUTO_LOW_RISK + the domain flag), while medium/high (finance, contracts, creates, email) still stage for approval. (Round 2 had force-queued all of them; that inconsistency is gone.)
  3. Server-side aggregate RPCs β€” the crm_safe_query rollups (pipeline, funnel, consent, status counts…) now aggregate in one SQL function instead of paging rows, so counts are exact on large tables. Pagination stays as an automatic fallback.

Aldilaijan & Khobara Real Estate Platform