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/andsupabase/functions/, backed by Supabasejrsgosnnyjonxaesqtln. 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_RISKandHERMES_WHATSAPP_TASK_DISPATCH_ENABLEDare alltruein production, so the "default off" notes below are stale.The
crm_*MCP surface this document describes β the "81 tools", thecrm_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_ENABLEDandHERMES_PUBLIC_MCP_ENABLEDaretrueon 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 (
normalizePhonestrips+, so+965β¦matches965β¦). Empty/unsetHERMES_WHATSAPP_ADMIN_PHONES= Admin Mode disabled. - Two conditions, not one. The allowlist says WHO;
ADMIN_ENTITYsays WHERE. An allowlisted phone texting a customer line is a customer β the publicaldilaijannumber 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 inwhatsapp-webhook'sgetEntityConfig()from the env slotsWHATSAPP_PHONE_NUMBER_ID_{PERSONAL,ALDILAIJAN,KHOBARA,ALDILAIJAN_EXEC,CLIENT,ADMIN}(+ matchingWHATSAPP_ACCESS_TOKEN_*).
Current admin setup (as configured) β
- Sender (admin):
+96562224000(ΨΉΨ¨Ψ―Ψ§ΩΨ±ΨΩ Ω β super admin). - Control number:
+96522466862(staff command line). - Entity:
admin. - Meta
phone_number_idfor 96522466862:1022182970975277.
β οΈ Retired entities:
aldilaijan_execandclient. Neither resolves to a line.aldilaijan_exechas noWHATSAPP_PHONE_NUMBER_ID_*secret at all, andWHATSAPP_PHONE_NUMBER_ID_CLIENTheld the same Meta id as_ALDILAIJAN, whichgetEntityConfig()checks first.ADMIN_ENTITYwas left onaldilaijan_execafter the Julyadmincutover, 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, checkADMIN_ENTITYfirst.
Done on the system side β
whatsapp_ai_configentityadminβ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)
| Secret | Value |
|---|---|
WHATSAPP_PHONE_NUMBER_ID_ADMIN | 1022182970975277 |
WHATSAPP_ACCESS_TOKEN_ADMIN | System-User token (falls back to _ALDILAIJAN) |
ADMIN_ENTITY | admin β must name an entity with a phone-number-id secret |
HERMES_WHATSAPP_ADMIN_PHONES | 96562224000 (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) β
- 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.
- Copy its
phone_number_id(e.g.1022182970975277for exec). - Subscribe the webhook:
- URL
https://jrsgosnnyjonxaesqtln.supabase.co/functions/v1/whatsapp-webhook - Verify token = existing
WHATSAPP_WEBHOOK_VERIFY_TOKEN - Subscribe the
messagesfield (and status fields you use).
- URL
- 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.
- Follow
WHATSAPP_COEXISTENCE.mdβ Embedded Signup β Connect your existing WhatsApp Business app, QR scan, secrets, deploy. - Subscribe webhook fields
messagesandsmb_message_echoes(phone-sent replies mirror into CRM). - Verify with Graph API:
is_on_biz_app=trueandplatform_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:
-- 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):
- Name search β
crm_safe_querycan find clients/deals/requests/tasks/ properties by name substring (no UUID needed). - Approve/reject from chat β
crm_list_pending_actions,crm_approve_action(approves AND executes),crm_reject_actionon theagent_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, fallbackprofiles.phone+ admin role) and injects a signed 15-minadmin_session_tokeneach 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. - 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 inpayload.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:
- 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.) - Command Center task dispatch β
crm_enqueue_task/crm_list_tasks/crm_get_task/crm_cancel_tasklet 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 byHERMES_WHATSAPP_TASK_DISPATCH_ENABLEDplus the runner'stasks.enabledand 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):
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.- 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 (withHERMES_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.) - Server-side aggregate RPCs β the
crm_safe_queryrollups (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.
