PII redaction
Redaction happens at the runner’s edge, after the connector answers and before the result crosses the boundary. A single enforcement point: not scattered, not repeated on the other side.
It is active when RUNNER_CONNECTORS=real. On the synthetic path the data carries no PII, so only the
declared-field layer runs.
The three layers, in this order
Section titled “The three layers, in this order”1. Declared field: FieldRedactor
Section titled “1. Declared field: FieldRedactor”Masks the paths the connector’s manifest declares in sensitiveFields. It is pure dot-path, by
design.
2. Structural: redactStructured
Section titled “2. Structural: redactStructured”An operation-keyed pass, for the shapes a regex cannot see: arbitrary property-bag values and raw end-user identifiers.
It covers exactly five Amplitude operations:
| Operation | Masks | Redacts |
|---|---|---|
analytics.searchUser |
userId |
properties |
analytics.getUserActivity |
none | properties, deviceId |
replays.listSessionReplays |
userId |
none |
replays.getSessionReplay |
userId |
none |
replays.getSessionReplayEvents |
none | properties |
In property bags, keys are preserved and values are redacted, and the shape of the data stays legible for diagnosis, the content does not.
For every other operation, this layer is a no-op.
3. Content: redactPii
Section titled “3. Content: redactPii”A final, content-aware regex pass over whatever strings are left: email, CPF, IPv4, credit card with Luhn validation, and phone numbers, plus a set of sensitive field names.
It is not dot-path driven, so it complements the two layers above and acts as defense in depth.
The trap: sensitiveFields doesn’t cross arrays
Section titled “The trap: sensitiveFields doesn’t cross arrays”This is the trap that most often produces a false guarantee, so let’s be direct.
FieldRedactor is pure dot-path. Declaring in the manifest a field that only exists inside an
array (say a field of a RumErrorSample[]) redacts nothing. And that is worse than not
declaring it, because it produces the appearance of a guarantee that isn’t there.
For that case, the rule belongs in redactStructured, which is operation-keyed and does cross
arrays.
The kill switch, and what it doesn’t disable
Section titled “The kill switch, and what it doesn’t disable”ROOTPILOT_PII_REDACTION=offThere is a legacy alias, INFRAINTEL_PII_REDACTION. When both are present, the new name wins.
Be precise about its reach:
| Layer | With ROOTPILOT_PII_REDACTION=off |
|---|---|
| 1. Declared field | still runs |
| 2. Structural | disabled |
| 3. Content | disabled |
The disabled layers audit the decision to stderr, so turning it off is not silent.
Divergent classification: the edge exposes, it doesn’t rewrite
Section titled “Divergent classification: the edge exposes, it doesn’t rewrite”One case that shows up in practice and is worth understanding before filing it as a bug.
Datadog sends all stderr to status:error. So a search for errors can return
[INFO] Worker exiting. The source classified it as an error, the text says otherwise.
The edge exposes the divergence rather than rewriting the field: alongside the source’s level, it
emits textLevel and levelMismatch. The principle is that the source’s classification is a fact
about the source, and silently correcting it would erase information.
Where to audit
Section titled “Where to audit”| What | Where |
|---|---|
| The three-layer composition | apps/runner/src/redact/pii-redactor.ts |
| Declared-field layer | apps/runner/src/redact/field-redactor.ts |
| Structural layer, per operation | packages/core-edge/src/policy/structured-redactor.ts |
| Content engine | packages/core-edge/src/policy/pii-redactor.ts |