# Mask PII in action input

`PiiInputValidator` and `PiiInputSanitizer` plug into [`@InputGuardrail`](guardrails.md#action-input-and-output) to reject or mask personal data before an action sees it. Both are in `tnsai-core`.

This page covers action input. To check traced text, see `PiiGuardrail` in [Guardrails](guardrails.md). To mask data on its way to logs, events and memory, see [Redaction](redaction.md).

## Mask PII before the action runs

<!-- java-contract: src/main/java/com/example/tnsai/docs/SecurityPiiInputExample.java -->
```java
public static class ContactActions {
    @ActionSpec(type = ActionType.LOCAL, description = "Forward a contact message")
    @InputGuardrail(
        validators = PiiInputValidator.class,
        sanitizers = PiiInputSanitizer.class,
        onFailure = InputGuardrail.FailureAction.SANITIZE)
    public String handleContact(String message) {
        return message;
    }
}
```

A call with `"Mail me at jane@example.com"` reaches the action as `"Mail me at [REDACTED_EMAIL]"`.

Validators and sanitizers need a public no-argument constructor. To add your own, implement `InputValidator` (`isValid`) or `InputSanitizer` (`sanitize`).

## What counts as PII

Detection is a high-precision heuristic, not a legal or jurisdiction-complete classifier.

| Detects | Replaced with |
|---------|---------------|
| Email addresses | `[REDACTED_EMAIL]` |
| Phone numbers with an international prefix, or 10-digit local numbers | `[REDACTED_PHONE]` |
| IBANs (country code and check digits) | `[REDACTED_IBAN]` |
| Payment card numbers of 13 to 19 digits that pass the Luhn check | `[REDACTED_PAYMENT_CARD]` |
| US social security numbers (`123-45-6789`) | `[REDACTED_NATIONAL_ID]` |
| Turkish identity numbers with a valid checksum | `[REDACTED_NATIONAL_ID]` |

`PiiInputValidator` rejects a value as soon as one class matches. It also checks parameter names, so a name that contains PII is rejected too.

## When a value is rejected

`onFailure` decides what happens. The default is `REJECT`.

| `onFailure` | Result |
|-------------|--------|
| `REJECT` | The call fails |
| `SANITIZE` | The sanitizers run in order, then the validators check the result again |
| `REVIEW` | The call fails and is marked as needing review |
| `WARN` | A warning is logged and the original value passes through |

A failed call surfaces as an `ActionExecutionException` in the `VALIDATION` category. Its cause is a `GuardrailViolationException` whose `guardrailCause()` is `INPUT_VALIDATOR_REJECTED`, or `NEEDS_REVIEW` for `REVIEW`. Diagnostics never include the matched text.

Strings are checked directly. Booleans, enums that render as their name, and standard JDK number types are checked in their text form. Lists, maps, arrays, other objects and enums with a custom `toString` cannot be rewritten safely, so they are rejected under every policy except `WARN`. Flatten such payloads into string parameters first.

## Where the check runs

The executor runs the guardrail after `@BeforeAction` and before any parameter encryption from `@Security`, so validators always see plain text. The same check applies to role actions, `@Tool` methods and MCP tools.

## Pattern limits

`blockPatterns` and `allowPatterns` on the same annotation use a restricted regex subset, to avoid slow matching:

- each pattern and each inspected value is limited to 1,024 characters;
- backreferences, nested or repeated quantifiers, comments mode and more than one alternation group are not allowed.

The built-in PII patterns are not subject to these limits.

## Related

- [Guardrails](guardrails.md)
- [Redact sensitive data](redaction.md)
