feat(enclave): Coyote Mode anti-spam (cross-room burst -> enclave ban + 24h soft-purge) (LC-339) #355

Merged
nrupard merged 2 commits from feat/lc-339-coyote-mode into main 2026-06-18 19:51:50 +02:00
Owner

What

Adds an opt-in per-enclave anti-spam feature, Coyote Mode, alongside the LC-217 rate limit. When an enclave manager enables it, a member who posts in 3+ distinct rooms of the enclave within <=3 seconds is treated as a bot:

  • Banned from the enclave - kicked and added to a new enclave_bans list so they cannot rejoin (no enclave-level ban existed before; kick alone lets a public enclave be rejoined instantly).
  • Their last-24h messages in that enclave's rooms are soft-deleted - reversible tombstone (deleted_at), logged to mod_actions.

Default off. Manager-gated (require_manage).

Scope decisions (with the product owner)

  • Enclave-only ban, not site-wide: an enclave manager must not be able to trigger a global ban via a heuristic (privilege escalation).
  • Purge limited to this enclave's rooms, last 24h.
  • Soft delete (recoverable), since automated detection can false-positive.
  • Enclave managers and site admins are exempt from detection (no self-lockout / power-user false positives).
  • Thresholds (3 rooms / 3s / 24h) are fixed per the spec.

Implementation

  • Migration 0064_coyote_mode.sql: enclaves.coyote_mode, enclave_bans table, messages(user_id, created_at) index.
  • db/enclave.rs: coyote_mode on the model + every SELECT/mapper; set_coyote_mode, get_coyote_mode_for_room, count_distinct_rooms_posted_recently, ban_from_enclave, is_enclave_banned. db/moderation.rs: soft_delete_user_messages_in_enclave (scoped batched soft-delete, returns purged ids).
  • Detection in routes::room::post_message after a successful insert, gated on the room's enclave having the mode on and the poster not being a manager/admin; a per-insert COUNT(DISTINCT room_id) over a 3s window (no AppState field -> no test-construction drift). The ban + purge runs in a tokio::spawn off the send path and broadcasts MessageDeleted per purged row + EnclaveMemberRemoved, scrubs the user's stars, and drops their enclave:{id} topic subscription (LC-176).
  • Enforcement: is_enclave_banned -> 403 on rejoin (post_join_by_code, post_discover_join) and on posting in the enclave's rooms.
  • Settings UI toggle + POST /enclave/{id}/coyote-mode handler; en/es i18n keys.

Test

  • New tests/db_coyote_mode.rs: toggle round-trip; ban kicks + blocks + idempotent; distinct-room counter is window- and enclave-scoped (other enclave / old message / other user excluded); soft-purge scoped to enclave + user + 24h, sets deleted_at, second run is a no-op.
  • just check (clippy standalone + saas + desktop, fmt), just test, just test-saas all pass. en/es i18n parity holds. Migration 0064 lands in common::chat_pool() automatically; the curated migration_enclaves.rs is unaffected.

Closes LC-339.

## What Adds an opt-in per-enclave anti-spam feature, **Coyote Mode**, alongside the LC-217 rate limit. When an enclave manager enables it, a member who posts in **3+ distinct rooms of the enclave within <=3 seconds** is treated as a bot: - **Banned from the enclave** - kicked and added to a new `enclave_bans` list so they cannot rejoin (no enclave-level ban existed before; kick alone lets a public enclave be rejoined instantly). - **Their last-24h messages in that enclave's rooms are soft-deleted** - reversible tombstone (`deleted_at`), logged to `mod_actions`. Default off. Manager-gated (`require_manage`). ## Scope decisions (with the product owner) - **Enclave-only ban**, not site-wide: an enclave manager must not be able to trigger a global ban via a heuristic (privilege escalation). - **Purge limited to this enclave's rooms**, last 24h. - **Soft delete** (recoverable), since automated detection can false-positive. - **Enclave managers and site admins are exempt** from detection (no self-lockout / power-user false positives). - Thresholds (3 rooms / 3s / 24h) are fixed per the spec. ## Implementation - Migration `0064_coyote_mode.sql`: `enclaves.coyote_mode`, `enclave_bans` table, `messages(user_id, created_at)` index. - `db/enclave.rs`: `coyote_mode` on the model + every SELECT/mapper; `set_coyote_mode`, `get_coyote_mode_for_room`, `count_distinct_rooms_posted_recently`, `ban_from_enclave`, `is_enclave_banned`. `db/moderation.rs`: `soft_delete_user_messages_in_enclave` (scoped batched soft-delete, returns purged ids). - Detection in `routes::room::post_message` after a successful insert, gated on the room's enclave having the mode on and the poster not being a manager/admin; a per-insert `COUNT(DISTINCT room_id)` over a 3s window (no AppState field -> no test-construction drift). The ban + purge runs in a `tokio::spawn` off the send path and broadcasts `MessageDeleted` per purged row + `EnclaveMemberRemoved`, scrubs the user's stars, and drops their `enclave:{id}` topic subscription (LC-176). - Enforcement: `is_enclave_banned` -> 403 on rejoin (`post_join_by_code`, `post_discover_join`) and on posting in the enclave's rooms. - Settings UI toggle + `POST /enclave/{id}/coyote-mode` handler; en/es i18n keys. ## Test - New `tests/db_coyote_mode.rs`: toggle round-trip; ban kicks + blocks + idempotent; distinct-room counter is window- and enclave-scoped (other enclave / old message / other user excluded); soft-purge scoped to enclave + user + 24h, sets `deleted_at`, second run is a no-op. - `just check` (clippy standalone + saas + desktop, fmt), `just test`, `just test-saas` all pass. en/es i18n parity holds. Migration 0064 lands in `common::chat_pool()` automatically; the curated `migration_enclaves.rs` is unaffected. Closes LC-339.
feat(enclave): Coyote Mode anti-spam (cross-room burst -> enclave ban + 24h soft-purge)
All checks were successful
check-secrets / Kingfisher (push) Successful in 4s
check-secrets / TruffleHog (push) Successful in 4s
check-secrets / Nosey parker (push) Successful in 4s
check-secrets / Nosey parker (pull_request) Successful in 3s
check-secrets / Kingfisher (pull_request) Successful in 4s
check-secrets / TruffleHog (pull_request) Successful in 5s
Check / clippy + fmt + tests (pull_request) Successful in 2m19s
c7b3cea39a
Adds an opt-in per-enclave anti-spam toggle, "Coyote Mode", alongside the LC-217 rate limit. When enabled, a member who posts in 3+ distinct rooms of the enclave within <=3 seconds is treated as a bot: banned from that enclave (kicked + added to a new enclave ban-list so they cannot rejoin) and their last-24h messages in the enclave's rooms are soft-deleted (reversible tombstone, logged to mod_actions). Default off; manager-gated.

Scope is deliberately enclave-only: no site-wide ban (an enclave manager must not be able to trigger a global ban via the heuristic), no cross-enclave purge, and soft - not hard - delete so a false positive is recoverable. Enclave managers and site admins are exempt from detection.

Implementation: migration 0064 adds enclaves.coyote_mode, an enclave_bans table, and a messages(user_id, created_at) index. Detection is a per-insert COUNT(DISTINCT room_id) over a 3s window (no AppState change, so no test-construction drift), gated to coyote-enabled enclaves; the ban + purge runs in a spawned task off the send path and broadcasts MessageDeleted per purged row + EnclaveMemberRemoved, and drops the banned user's enclave-topic subscription (LC-176). The ban is enforced on rejoin (invite code + discover) and on posting in the enclave's rooms.

#LC-339

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
test(enclave): HTTP coverage for Coyote Mode gate + ban enforcement; clarify exemption copy
All checks were successful
check-secrets / Nosey parker (push) Successful in 3s
check-secrets / Kingfisher (pull_request) Successful in 4s
check-secrets / Kingfisher (push) Successful in 5s
check-secrets / TruffleHog (push) Successful in 5s
check-secrets / Nosey parker (pull_request) Successful in 8s
check-secrets / TruffleHog (pull_request) Successful in 8s
Check / clippy + fmt + tests (pull_request) Successful in 5m41s
Create release / Create release from merged PR (pull_request) Has been skipped
d4f990e736
Code-review follow-up for LC-339.

Adds tests/routes_coyote_mode.rs: non-manager cannot toggle Coyote Mode (403, no effect); a manager can toggle it on/off; an enclave-banned user is blocked (403) from posting in the enclave's rooms and from rejoining via discover (with a pre-ban control post asserting the 403 is the ban, not another gate). The DB layer already covers detection/purge in db_coyote_mode.rs; the auto-ban trigger fires from a fire-and-forget spawn and is not asserted end-to-end.

Also corrects the settings help copy (en + es): the exemption is all enclave managers (owners and admins) plus site admins, not just "admins".

#LC-339

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
nrupard deleted branch feat/lc-339-coyote-mode 2026-06-18 19:51:50 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/lets-chat!355
No description provided.