Skip to main content

Redactions

A Redactions store is one conversation's private memory of the personal data that jes hid from the model.

Its job​

When pii() finds a value such as an email address, jes replaces it with a placeholder before the text reaches your model or any judge. The model works with [JES_PII_…] instead of ana@example.com.

The real value has to live somewhere, or nothing could put it back. That place is the Redactions store. It has three jobs:

  1. Remember: map each placeholder to the value it replaced.
  2. Restore: when the model's reply uses a placeholder, check_output puts the real value back for the user.
  3. Stay consistent: the same value always gets the same placeholder in one store, so across many turns the model sees one stable stand-in.

It does not decide what counts as personal data. That is the job of the policies (pii, secrets). It also never hands values out on request. Values leave only through output restoration, or encrypted through dumps.

Mental model​

A store is a coat check for one conversation. jes takes the value, gives the model a ticket, and hands the value back only when that ticket comes back in a reply.

A ticket is an HMAC of the value under the store's own random secret. No other store can produce or redeem it. A placeholder that a user types, or that turns up in a retrieved document, is never trusted. It is treated as literal text and flagged.

Using it​

Create one store per conversation, and pass it to every check in that conversation:

from jes import Guard, Redactions
from jes.policies import pii

guard = Guard([pii(["EMAIL_ADDRESS"])])
store = Redactions(scope=b"tenant-42:conversation-7") # one per conversation

incoming = guard.check_input("Email me at ana@example.com", redactions=store)
reply = call_model(incoming.onward) # the model sees "[JES_PII_…]"

outgoing = guard.check_output(reply, prompt=incoming, redactions=store)
show_user(outgoing.onward) # the real email is back

If your app is stateless between requests, save the store encrypted, and load it on the next turn. This needs jes[crypto]:

blob = store.dumps(key, associated_data=b"tenant-42") # key: 32 bytes
# … next request …
store = Redactions.loads(
blob, key, scope=b"tenant-42:conversation-7", associated_data=b"tenant-42"
)

Good to know​

  • One store per conversation.
    • A store shared across conversations lets one conversation's tickets resolve in another.
    • Passing a store that doesn't match the one on the prompt= result raises RedactionError.
  • You can skip the store for a single turn. Without one, check_input creates an in-memory store. It is kept on result.redactions and reused when you pass that result as prompt=.
  • A blocked check leaves the store unchanged. New entries are committed only when the check is complete and allowed.
  • It can't be copied or pickled. This is on purpose, so values don't leak through a copy. Use dumps.
  • It is bounded.
    • Past max_entries (1,000) or max_bytes (8 MiB), a check blocks with redaction_store_full.
    • More than Limits(max_redactions=...) values in one check block with too_many_redactions.
    • Neither raises an error.
  • Tool-call arguments are never restored. jes doesn't fill real values into a tool call. Your code authorizes and fills tool inputs.

Next​