# SPEC-003 — Safety, privacy + abuse hardening FDB-005 lands the injection-defense subset (the old `hack` self-report was the only defense — S4). Consequential actions are gated *outside* the model: the model proposes, deterministic code disposes. Quotas, budget caps, memory policies and GDPR controls follow with FDB-014 (SAF-10+, reserved). ### SAF-01 — Model-proposed channel routing is allowlisted (coverage: test) A model-proposed answer channel is honored only when it is in the allowed set: config `allowed-channels` when set, otherwise the channels already named in config (`chat-channel`, `staff-channel`, `welcome-channel`, `additional-responders`). Anything else falls back to the origin channel and is logged. Prompt injection must not be able to redirect the bot into arbitrary channels. ### SAF-02 — Outbound messages cannot ping (coverage: test) The bot is constructed with `allowed_mentions = none`: no user, role or @everyone/@here pings in any outbound message, regardless of what the model emits. ### SAF-03 — External text is sanitized before prompting (coverage: test) Text from external sources injected into prompts or tool results — news-feed content and IGDB results — passes `sanitize_external_text`: control characters stripped, `@everyone`/`@here` neutralized with a zero-width space, length capped (default 4000 chars). RSS headlines and game descriptions are attacker-influenced input. The `hack` envelope field remains as an advisory signal (logged, staff-notified) but is no longer the defense. ### SAF-10 — Model calls carry a hashed user identifier (coverage: test) Chat calls pass `safety_identifier` = a short SHA-256 digest of the message author's name — OpenAI-side abuse tracing without shipping raw Discord identities (Codex review recommendation). ### SAF-04 — Hard daily budget, fail-closed (coverage: test) When `daily-budget-usd` is configured and today's estimated spend reaches it, no further model or image calls happen: the responder refuses before calling the API, and the bot answers nothing until midnight. Staff get exactly one alert per day about the silence. Cost estimation uses `price-input-per-m` (default 1.0), `price-output-per-m` (default 6.0) and `price-per-image` (default 0.05). A budget of 0 means fully silent — fail-closed by construction. ### SAF-05 — Usage is metered and survives restarts (coverage: test) Token counts (prompt/completion) from every model call and every generated image are recorded per calendar day; with a store configured the counters persist across restarts (usage table, schema v2). ### SAF-06 — Per-user daily message quota (coverage: test) When `user-daily-messages` is configured, messages beyond the cap from one user on one day are ignored (logged, no model call). The `system` user (bot-initiated flows) is exempt. ### SAF-07 — Per-user daily image quota (coverage: test) When `user-daily-images` is configured, picture requests beyond the user's daily cap are stripped from the response (the text answer still goes out). ### SAF-08 — !forgetme purges a user's history (coverage: test) `!forgetme` removes the requesting user's messages from all live responder histories and from the store, then confirms in-channel. Since FDB-007 the purge extends to memory itself — facts, observations and episode traces (MEM-09). ### SAF-09 — !privacy states the data practice (coverage: test) `!privacy` answers with the configured `privacy-notice` (a default notice ships in code): what is stored, that `!forgetme` exists. Works even while the bot is paused. ### SAF-11 — Hack self-report ignored for the system user (coverage: test) The `hack` envelope flag is meaningless on bot-initiated flows: the `system` user is the scheduler, not a person, so a self-report there is by definition a false positive (observed live after enabling reasoning — the model flagged its own scheduled task prompts as impersonation and alerted staff). For `system` messages the flag is dropped: no warning log, no staff fallback alert. Model-authored `staff` text is NOT suppressed (OPS-07: alerts are never silently dropped). At the source, scheduled task prompts are prefixed with an internal-task note so the model need not guess who "system" is.