a6e934e0fd
Port from qwibitai/nanoclaw#3260: adds a third unauthorized_dm_behavior option, 'decline'. Instead of replying with a pairing code (pair) or staying silent (ignore), the gateway sends one short, polite decline to the unknown sender, then stays silent toward that sender for 24 hours. - gateway/config.py: accept 'decline' in the normalizer; new unauthorized_dm_decline_message for custom decline text (round-trips through to_dict/from_dict). - gateway/pairing.py: persisted decline stamps (_declined.json) on PairingStore with has_recent_decline/record_decline; stamps are pruned on write and recorded BEFORE delivery so a send failure can't become a decline storm (nanoclaw's stamp-first pattern). - gateway/run.py: decline branch in the unauthorized-sender path; groups still always silently ignore. - docs: security.md + configuration.md updated. Adapted from TypeScript (NanoClaw's pending_sender_approvals 'decline:' stamp rows) to Hermes' existing PairingStore JSON persistence; the owner-FYI half of nanoclaw's flow is intentionally not ported — Hermes logs the unauthorized attempt, and pairing remains the owner-visible grant path. Rebase onto the decomposed gateway (salvage, #88028): - The unauthorized-sender path moved from gateway/run.py to gateway/run_inbound.py::_hm_admit_event; the decline branch is a sibling helper _hm_send_unauthorized_decline next to _hm_offer_pairing_code. - gateway/config.py now validates the enum via _normalize_choice; the accepted set is the module constant UNAUTHORIZED_DM_BEHAVIORS (used by both from_dict and get_unauthorized_dm_behavior so a per-platform `extra.unauthorized_dm_behavior: decline` is honoured too). The default decline text lives in config as DEFAULT_UNAUTHORIZED_DM_DECLINE_MESSAGE. - Tests trimmed from 5 to 2 invariant tests (send-once-then-silent through the real inbound path; config round-trip + real PairingStore stamp lifecycle with a patched clock instead of rewriting the JSON file).