Messaging Compliance Gate
Purpose
Consolidates per-send compliance decisions into one function so no outbound path can skip them. Reads from five Supabase tables to produce a deterministic allow/deny decision for each intended send. Fails closed on any error.
Replaces per-vendor or per-worker ad-hoc compliance logic. No outbound send path in OpenClaw may bypass this gate.
When to use
- As the gate function called by
bulk-sms-split-test.js,smart-outreach-worker.js, and future vendor send paths - As a standalone CLI tool to debug a specific phone/body/provider combination
- As the pre-check in the
/sms-opt-inAPI route for inbound consent handling (checks if the phone is already suppressed before recording new consent)
Do NOT use for:
- Post-send auditing (the gate is pre-send only)
- Webhook ingestion compliance (suppression writes happen in handlers, not this gate)
Inputs
Function call:
const gate = require('/home/opsadmin/.openclaw/tools/messaging-compliance-gate');
const decision = await gate.complianceGate({
phone: '+1XXXXXXXXXX', // E.164 required
body: '...', // rendered template body
provider: 'sent_dm', // messaging_providers.name
sender_number: '+1YYYYYYYYYY' // from which number; optional if provider has one default
});
// decision: { allow: boolean, reason?: string, details?: object }CLI:
node messaging-compliance-gate.js \
--phone=+1... --body='...' --provider=sent_dm
Outputs
Returns a JSON object:
{ allow: true }on pass{ allow: false, reason: '<enum>', details: {...} }on block
Reason enums:
suppressed— contact has STOP/compliance suppression on filequiet_hours— send attempted 21:00-08:00 recipient-local time (area-code → TZ mapping; standard-time offsets used, conservative in DST)cooldown— a prior outbound to same phone within 24h AND no inbound reply sinceduplicate_content— same SHA256(body) to same phone within last 1hno_campaign— provider has no approved TCR campaign row inmessaging_campaignsinvalid_phone— phone does not match^\+1\d{10}$no_consent— phone has nosms_opt_in=truerow incontact_consent
Acceptance tests
- A phone with a
stop_keywordrow inmessaging_suppression_events(active partial-unique index) always returns{ allow: false, reason: 'suppressed' } - Send attempted at 22:00 local time blocks with
reason: quiet_hours - Second send attempt within 24h to same phone when no reply happened in between blocks with
reason: cooldown - Second send of same body-hash to same phone within 1h blocks with
reason: duplicate_content - Provider without a row in
messaging_campaignsblocks withreason: no_campaign - Phone
"(555) 123-4567"blocks withreason: invalid_phone(not E.164) - Phone with no consent row or
sms_opt_in=falseblocks withreason: no_consent - Calling twice with identical inputs returns same decision (idempotent)
- DB error or timeout: returns
{ allow: false, reason: 'gate_error' }(fail closed)
Rollback behavior
Read-only gate; no writes to rollback. Calling code is responsible for logging gate decisions to its own audit trail.
If gate is malfunctioning (blocking everything) in production:
- Set
MESSAGING_COMPLIANCE_GATE_BYPASS=truein master.env - Re-source + restart consumers
- Investigate + fix root cause
- Unset bypass
Bypass should be logged as an incident in workspace/reports/compliance-gate-bypass/<date>.md including reason, scope, duration.
Related files
- Plan section: Section B (no-send gate) + Section G.4 in
/home/opsadmin/.claude/plans/put-you-full-last-functional-sparrow.md - Implementation:
/home/opsadmin/.openclaw/tools/messaging-compliance-gate.js - Reads from:
messaging_suppression_events,messaging_outbound_messages,messaging_campaigns,contact_consent - Uses: area-code-to-UTC-offset map (US codes, standard-time offsets)
- Consumers:
bulk-sms-split-test.js,smart-outreach-worker.js, future/sms-opt-inAPI - Gate doc:
workspace/docs/NO-SEND-GATE-v1.md