AI defense
The layered gates every AI call passes through — kill switch, prompt-injection detection, replay protection, and output filtering.
AI defense
Every AI call in Pact runs through a stack of strict gates before and after the
model is invoked. Each gate defends a specific vector: cost, behaviour, replay,
and leakage. Together they map to the OWASP Top 10 for LLM Applications. The
whole stack is wired into core.ai.client.AIClient and enforced on every call —
this is not an advisory layer.
Enforced on every call
The gates below run inside the AI client. A tripped gate raises before (or instead of showing) the model response — there is no path that skips them for a normal feature call.
The gate order
kill switch → data-sharing redaction → budget/rate cap
→ injection detection → replay protection
→ [ model call ] → output filter
1. Kill switch — the break-glass control
core.ai.kill_switch is the outermost gate. Three levels:
- Global — the
AI_SUBSYSTEM_DISABLEDenv var. When truthy, every tenant's AI is disabled (incident response, provider outage, secret rotation). - Per-tenant — the
tenant_ai_budget.disabled_reasoncolumn (alembic0153_ai_kill_switch). Non-NULL means that tenant's AI calls fail fast with a clear "AI temporarily disabled" error — no model call, no ledger row, no defense overhead. - Auto-trigger — engages the per-tenant switch when suspicious activity
crosses a threshold: 3+ injection blocks or 10+ rate-limit blocks in
the last 5 minutes. It writes an
auto_engagedrow toai_kill_switch_events; recovery is always manual.
2. Prompt-injection detection
core.ai.injection_detector rejects user content trying to override the system
prompt (OWASP LLM01). Coverage: direct override ("ignore all previous
instructions"), role-confusion tokens (Assistant: / </system>), delimiter
spoofing (###, [INST], ---END---), Unicode tag-character smuggling
(U+E0000…U+E007F, invisible to renderers but legible to the model), and
suspicious base64 blobs (flagged for review). score_content returns a
confidence in 0.0…1.0; the default policy blocks at >= 0.8
(DEFAULT_BLOCK_THRESHOLD). Blocks are recorded to ai_injection_blocks
(alembic 0150_ai_injection_blocks), which also feeds the kill-switch
auto-trigger.
Pattern detector, not a model classifier
Injection detection is a zero-cost, near-zero-latency pattern detector. A secondary model-based classifier is on the roadmap but has a meta-problem — an attacker who controls the input controls what reaches the classifier too — so Pact ships the pattern detector first and would treat any future classifier output as a signal, not a verdict.
3. Replay protection
core.ai.replay_protection blocks captured-request replay (a leaked or
intercepted signed request replayed later — a 5-minute freshness window by
default) and cross-tenant idempotency-key collisions, caught before the call
reaches the spend ledger.
4. Output filter
core.ai.output_filter runs after the model responds and catches the case
where an injection got through and the model complied: system-prompt leaks,
jailbreak-success markers ("In DAN mode…", "Bypassing my filters…"), and refusal
pretext (refusing while including the very content being refused). It scans
tool-use inputs too. When score_response flags a response, the caller does not
show it, writes a row to ai_output_filter_blocks, and surfaces it for triage.
Admin visibility
Where you see the events
The kill-switch state, its recent events, and the auto-trigger spikes (which are
driven by the injection- and rate-block tables) are surfaced today on the
Admin → Security → AI budget page — the
security review lands there. A dedicated per-gate triage dashboard for raw
injection and output-filter blocks is a roadmap refinement; the underlying
ai_injection_blocks and ai_output_filter_blocks tables
are already populated and auditable.