fahmek
In this guide

fahmek: architecture and data flow

For the security committee. Engine version: see engine_version in any API response.

1. What the service does, in one sentence#

fahmek sits between the organization's LLM gateway (LiteLLM, Portkey, Kong…) and the model providers, and replaces Moroccan personal identifiers in messages with opaque tokens before the text leaves the perimeter. Identifiers that are detected and masked by the policy are replaced with tokens. Coverage is not absolute: an identifier without context can stay below the threshold, and a fail-open policy can let a request through during an outage. The per-register evaluation must measure these limits before any coverage promise.

2. Components#

ComponentRoleNetwork
fahmek (HTTP service, Python/FastAPI)Detection, decision, masking, restorationListens on the internal network only
Redis (optional)Re-identification map token → value, in memory, per tenant, short lifetimeInternal only, no persistence
LiteLLM guardrail (integrations/litellm/fahmek_guardrail.py, or fahmek's "Generic Guardrail" API from LiteLLM's web UI)Calls fahmek before the model call and after the answerTalks to fahmek and to the proxy, never to the provider
auto-gad + LanguageTool (optional)Fixes French and English spelling of the already-masked text; never blocks, never logs the textInternal only, self-hosted LanguageTool
Recognizer packs (recognizers/*.yaml)Patterns, context words, checksums, test casesFiles loaded at startup
Policies (policies/*.yaml)Active entities, actions, thresholds, fail_open, reversibility TTLFiles loaded at startup

The service ships no machine-learning model: regular expressions, checksums (mod 97, ISO 7064) and context words. It makes no network call on the request path, apart from Redis when reversibility is explicitly requested. On Kubernetes it reads, once at start, the ID of the cluster's kube-system namespace from the cluster's own API server, to check a license bound to that cluster; this never leaves the cluster. No license server, no download at runtime: it runs isolated from the network (air-gapped). The only outbound connection is the license integrity report, off unless the customer turns it on: every 4 hours, one HTTPS request to fahmek.com with the license status only (no text, no personal data), from a background task. The license works the same whether it gets through or not.

3. How a request flows#

user ──▶ LLM gateway ──(1)──▶ fahmek /v1/inspect ──▶ decision
                                    │
          masked text ◀──(2)────────┘
               │
              (3) ──▶ model provider (sees tokens only)
               │
          answer ──(4)──▶ fahmek /v1/restore ──(5)──▶ user
  1. Inspection. The gateway sends every text message bound for the provider (user, system, developer, assistant, tool) to POST /v1/inspect with the policy and tenant identifiers and an opaque namespace of its own per message. Two identifiers of the same type in different messages therefore never share a token. OpenAI arrays of type: text parts are joined for inspection, then sent back as one masked text part. With the integration kit, function-call arguments are decoded, inspected and re-encoded without changing their structure; from LiteLLM's web UI, a request holding an identifier there is refused. Images, audio and unknown formats are refused (400).
  2. Three-valued decision. allow, redact or block, never a boolean. redact returns the original text with the values replaced by «ENTITY_n» tokens; block never returns text.
  3. Forwarding. The allowed or masked text goes to the provider.
  4. Restoration (when reversible: true). The model's answer goes through POST /v1/restore; tokens are replaced with the original values, streamed answers included.
  5. Answer to the user, with their identifiers intact.

Inside an inspection#

  1. Normalization: NFKC, Arabic-Indic digits (٠١٢٣, ۰۱۲۳) converted to ASCII, tatweel and bidirectional control characters (U+202E…) removed. An index map links each position of the normalized text to the original position, in Unicode codepoints. This defeats the usual evasions: digits in Arabic script, a bidi control inserted in the middle of a number.
  2. Detection on the normalized text: patterns, then checksum (a RIB with an invalid key is not a RIB), then confidence adjusted by context words and best-effort prefix lists.
  3. Policy: action per entity (inherited from the pack or overridden), thresholds per action, safe cascade (a block below its threshold becomes redact, never allow).
  4. Masking on the original: positions are projected back onto the original text; the unmasked parts are byte-for-byte identical to the input. The normalized text never leaves the service.

4. What is stored, and where#

DataLocationDurationLogged?
Request textNowhere—No
Detected valuesNowhere unless reversible: true—Never
token → value map (reversibility)Redis, in memory, key derived from tenant_id + request_id (SHA-256)Policy TTL (300 s by default), consumed by the first restoreNo
Results (findings)HTTP response only: type, positions, confidence, action—Up to the gateway

Non-negotiable rule: Redis runs with save "" and appendonly no. fahmek checks these settings at startup; it warns loudly by default and refuses to start when a loaded policy is fail_open: false. A deployment with persistence enabled breaks the contract.

Isolation between tenants: tenant B cannot restore a request of tenant A (a different key by construction); the answer is the same whether a map does not exist or belongs to another tenant, so nothing reveals that it exists.

5. Failure modes#

SituationBehaviorSignal
Redis unreachable, reversible requested, fail_open: true policyMasking done; only reversibility degradeddegraded: ["reversible"]
Same, fail_open: false policyblock decision, no text returneddegraded: ["reversible"]
Engine errorallow (uninspected text) if fail_open: true, otherwise blockfindings: []
fahmek unreachable from the gatewayThe guardrail follows the policy's fail_open, cached from GET /v1/policies; unknown policy ⇒ refusalLog fahmek.degraded / fahmek.unreachable
Restoration impossibleThe answer is returned with its tokens (the values stay protected)Log fahmek.degraded stage=post_call

The safe direction is always the same: when in doubt, mask more, never leak.

6. Stated limits#

Detection is probabilistic for identifiers without a public specification (CIN, CNSS) and shape-only for those whose check algorithm is not published (ICE). The detection catalogue, generated from the packs, gives each identifier its confidence level and the exact wording of its limits. Names, addresses and dates of birth are not detected. Out of scope in v1: Arabic named-entity recognition, prompt-injection detection, detection of personal data newly generated in answers, image and audio inspection, control plane UI.

A guardrail or callback that sends text off site (a cloud scanner such as Prisma AIRS) must run after fahmek for the request and before fahmek for the answer; otherwise it receives the original values. LiteLLM's guardrail order and the settings to avoid are in Beside other guardrails.

7. Evidence#

  • Automated test suite run on every change: normalization (round-trip property on arbitrary inputs), packs (cases in the five language registers, negatives included), API (reference request/response pairs), reversibility (byte-for-byte identical round trip, expiry, Redis down, tenant isolation), guardrail, latency budget (p99 < 15 ms, measured per register at 4,096 and 8,192 codepoints).
  • End-to-end integration test: full compose stack, payload received by the provider checked free of original identifiers, on several LiteLLM releases (integrations/litellm/COMPATIBILITY.md).
  • Order beside another guardrail: LiteLLM's Prisma AIRS guardrail, pointed at a stand-in service that records what it receives, only sees masked text in the recommended order (tests/test_litellm_third_guardrail.py).
  • No real personal data in the repository: every test set is synthetic, with reserved or fictitious prefixes.

This page is the guide shipped with fahmek, kept in sync with each release.