Search

↑↓ to move↵ to openesc to closetop 6 documentation results

Book a demoTry TokenTriage

In development · Explore the product preview. Join the launch list or book a walkthrough.

Browse docs

Tokenize PII and get it back

A working reversible round trip — the config, the environment variable, a real request pair, both ledger rows, and the one trade you are making against redact.

How-to guideLast updated

On this page

Goal: replace detected email addresses in supported request fields before the provider call, then restore this request’s minted placeholders in a buffered answer.

The example uses the default on_error: open: failed inspection can forward original bytes. For a requirement to withhold content when inspection cannot finish, configure acknowledged on_error: closed and verify its failure cases as well as the successful round trip.

The trade in one sentence: reversibility buys you a restored answer and costs you a vault holding real values for the life of the request. redact has nothing to leak because it kept nothing.

1. Configure the forward verb only

There is no re-identify binding to write. Bind tokenize on the input phase and the reverse leg runs on the output phase by itself — a config naming action: reidentify is refused at boot.

guardrails:
  enabled: true
  default_profile: standard
  local_budget_ms: 50
  vault:
    key_env: TT_VAULT_KEY
  guards:
    - id: pii_tokenize_in
      category: pii
      phases: [input]
      detectors: [pii.email, pii.card]
      mode: enforce
      action: tokenize
      rule_ref: POL-PII-REID-1
  profiles:
    standard: [pii_tokenize_in]

Validate before you arm it. guardrails explain loads the config, so a bad file fails here rather than at boot:

$ tokentriage guardrails explain --binding pii_tokenize_in --config guarded.yaml

BINDING pii_tokenize_in
  category:   pii
  phases:     input
  mode:       enforce
  on_error:   open

  LADDER — the verb each severity resolves to (fall-down included)
    SEVERITY  ACTION    REVERSIBLE  THIS BUILD CAN DO IT
    low       tokenize  yes         yes

2. Start it with the key in the environment

$ export TT_VAULT_KEY="$(head -c 32 /dev/urandom | base64)"
$ tokentriage run -c guarded.yaml

3. Send a request

$ curl -s http://127.0.0.1:4790/v1/chat/completions \
    -H 'Content-Type: application/json' \
    -H 'Authorization: Bearer sk-…' \
    -d '{"model":"gpt-4o-mini","messages":[
          {"role":"user","content":"my email is [email protected]"}]}'

What the upstream received:

"my email is [TTsbjk:EMAIL_1]"

What your client received:

{"choices":[{"message":{"role":"assistant",
  "content":"Sure — I have noted my email is [email protected] and will follow up there."}}]}

The TTsbjk part contains a fresh random nonce drawn for each request; collisions remain possible. Isolation comes from the request-local minted mappings. Within one request the same value always maps to the same placeholder:

"ping [email protected] and [email protected] again"
   ⇒ "ping [TTj5xz:EMAIL_1] and [TTj5xz:EMAIL_1] again"

4. Read both legs on the ledger

Two kind: guardrail rows, same req_id. The forward leg names the detector and its finding; the reverse leg has no detector, because none runs on it:

{"v":1,"kind":"guardrail","req_id":"tt_o2fjhgarhtsbm2xk67ekejljdi",
 "binding":"pii_tokenize_in","phase":"input","detector":"pii.email",
 "detector_version":"pii.email-v1","action":"tokenize",
 "findings":[{"entity":"EMAIL","spans":[{"field":"messages[0].content","off":12,"len":15}],
   "raw_score":0.85,"confidence":{"basis":"reported","point":0.85}}],
 "posture":"open","mode":"enforce"}

{"v":1,"kind":"guardrail","req_id":"tt_o2fjhgarhtsbm2xk67ekejljdi",
 "binding":"pii_tokenize_in","phase":"output","detector":"","detector_version":"",
 "action":"reidentify","posture":"open","mode":"enforce"}

The reverse leg carries the forward binding’s id, which is what lets you join the two halves of one round trip without inventing a correlation field.

5. If you want a realistic surrogate instead

Swap the verb. Same config, same round trip, different bytes upstream:

      action: pseudonymize
UPSTREAM SAW: my email is [email protected]

Both verbs are equally reversible; pick pseudonymize when a bracketed marker would confuse the model and tokenize when a realistic-looking value would.

What can go wrong

  • The model mangles the placeholder. Exact match only, so the mangled marker reaches your client verbatim. Ugly, and the safe failure.
  • The response came from the cache. The reverse leg never runs on replayed bytes — restoring another request’s body is a cross-request leak, not a no-op.
  • You are streaming. The reverse leg is the buffered output gate’s; it is not implemented on the stream lane.
  • You expected fuzzy_reid to help. It is accepted and read by nothing.