🛡️ feat: Configurable Message PII Filter#13602
Conversation
There was a problem hiding this comment.
Pull request overview
Adds an opt-in, config-driven middleware to detect credential-shaped patterns in agent chat input and block requests early (HTTP 400) before moderation/model/persistence, with YAML/schema support for starter + custom regex patterns.
Changes:
- Introduces
createMessagePiiFilterExpress middleware with a starter catalog + operator-defined regex patterns. - Extends config schema/types to accept
messagePiiFilterand threads it into the resolved AppConfig (req.config). - Mounts the middleware on the agents chat router ahead of
moderateText, and documents usage inlibrechat.example.yaml.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/data-schemas/src/types/app.ts | Adds messagePiiFilter to AppConfig typing. |
| packages/data-schemas/src/app/service.ts | Exposes messagePiiFilter on the resolved AppConfig used by req.config. |
| packages/data-provider/src/config.ts | Adds Zod schema/type for messagePiiFilter and custom regex validation. |
| packages/api/src/middleware/messagePiiFilter.ts | Implements the new PII filter middleware and starter patterns. |
| packages/api/src/middleware/index.ts | Re-exports the new middleware from the API package. |
| api/server/routes/agents/chat.js | Mounts the middleware on agent chat routes before moderateText. |
| librechat.example.yaml | Documents configuration examples for messagePiiFilter. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Adds an opt-in `messagePiiFilter` middleware mounted on the agent chat route ahead of `moderateText`. When the configured patterns match the user's input the request is refused with 400, so the credential never reaches OpenAI moderation, the model, or MongoDB. Three starter patterns ship by default and operators can subset them or add their own regex via `customPatterns` in librechat.yaml.
31c8348 to
7f638b5
Compare
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7f638b503a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Memoize the compiled pattern array via a WeakMap keyed by the messagePiiFilter config object so repeat requests against the same config skip the per-request RegExp construction. Cache entries are released automatically when the config object itself rotates. Adds packages/api/src/middleware/messagePiiFilter.spec.ts covering the default-starter rejections, the starterPatterns subset and empty-array semantics, customPatterns matching layered on top of and in place of the starters, the no-config and empty-text pass-through paths, and a memoization regression check.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ea446b12ae
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…quest Admin DB overrides for `messagePiiFilter.customPatterns` reach `req.config` via `mergeConfigOverrides`, which deep-merges raw override values without re-running `configSchema`. A typo'd regex like `(` would slip past the YAML-load validation and throw inside `new RegExp(...)` during `compile()`, returning 500 for every chat request until the operator rolled the override back. Wrapped the per-pattern compile in a try/catch that logs the invalid pattern id + reason and skips it, so other valid patterns (starters and other custom entries) keep filtering. Added a regression test alongside the existing spec.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 81b37a1a9e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The chat-route middleware operates on `req.body.text`, but the remote agent API endpoints (`/api/agents/v1/chat/completions`, `/api/agents/v1/responses`) accept the same prompt content as a `messages` array or an `input` field. A caller using their API key could send a credential-shaped value through either route and bypass the configured PII filter even though they share the same agent and model backbone the middleware is meant to guard. Factored out `findPiiMatchInMessages`, a tolerant walker that handles both `content: string` and `content: ContentPart[]` user-message shapes against the same compiled, cached pattern list. Wired it into the OpenAI-compat controller after agent lookup and into the Responses controller right after `convertToInternalMessages`. Each returns the endpoint's native 400 error shape (`sendErrorResponse` / `sendResponsesErrorResponse`) with the `message_pii_filter_block` code when a user message matches.
|
@codex review |
|
Codex Review: Didn't find any major issues. Hooray! ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
…ocks The OpenAI-compat and Responses controller specs mock `@librechat/api` with a hand-listed object. The new `findPiiMatchInMessages` export wired into both controllers in 3ea35af was missing from those mocks, so the production lookup returned undefined and the controllers threw at request time under jest. Added the missing entries (default mock: returns null so the handlers fall through to the existing happy paths). All 278 agents-controller tests pass locally.
…import order
Renames the yaml field `messagePiiFilter` to `messageFilter.pii`, the
module to `messageFilterPii`, the factory to `createMessageFilterPii`,
the type to `MessageFilterPiiConfig`, and the error code to
`message_filter_pii_block`. The wrapper `messageFilter` namespace
gives future safety filters (e.g. `messageFilter.toxicity`) a place
to plug in without restructuring the config later. The
`findPiiMatchInMessages` helper kept its name because it already
describes what it does at the value level.
Also fixes import order Danny flagged on the OpenAI-compatible and
Responses controllers: `findPiiMatchInMessages` was appended at the
bottom of two `require('@librechat/api')` destructures rather than
placed in the length-sorted slot the house style expects.
Renames the docs page to match the upstream config rename in danny-avila/LibreChat#13602: messagePiiFilter is now nested under messageFilter as `messageFilter.pii`, and the page describes the namespace so future filter types (e.g. toxicity) have a documented home. Updates the top-level config.mdx anchor and the object_structure navigation accordingly. Error code in all examples is now `message_filter_pii_block`.
Renames the docs page to match the upstream config rename in danny-avila/LibreChat#13602: messagePiiFilter is now nested under messageFilter as `messageFilter.pii`, and the page describes the namespace so future filter types (e.g. toxicity) have a documented home. Updates the top-level config.mdx anchor and the object_structure navigation accordingly. Error code in all examples is now `message_filter_pii_block`.
Reorders the general sub-group inside the `require('@librechat/api')`
destructure shortest to longest so the whole block conforms to the
length-sort rule the file's `// Responses API` sub-group already
follows. Pure reorder, no other changes.
Reorders the `defaultConfig` keys in `packages/data-schemas/src/app/service.ts` shortest-line to longest-line, with the explicit-value entries (`mcpConfig`, `fileStrategies`, `cloudfront`) trailing the shorthand ones. Pure reorder, no behavior change.
|
This is great! Should we do this filtering for tool call outputs too? Another deterministic check we can do is check for invisible or suspiscious unicode characters in the output of tool calls and LLMs and remove them. This is used to present phishing web addresses to users. Non deterministic, more complex checks could be integrated through API calls - for example google's model armor or a self-hosted Prompt-Guard-86M model for example. |
|
@codex review |
|
@upman I think google's model armor is a good potential solution we can implement. I recently updated LibreChat to utilize hooks that we can tap into similar to Claude for this |
|
Codex Review: Didn't find any major issues. Nice work! ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
* 🛡️ feat: Reject chat messages matching configured credential patterns Adds an opt-in `messagePiiFilter` middleware mounted on the agent chat route ahead of `moderateText`. When the configured patterns match the user's input the request is refused with 400, so the credential never reaches OpenAI moderation, the model, or MongoDB. Three starter patterns ship by default and operators can subset them or add their own regex via `customPatterns` in librechat.yaml. * 🧪 test: Memoize compiled patterns + add middleware spec Memoize the compiled pattern array via a WeakMap keyed by the messagePiiFilter config object so repeat requests against the same config skip the per-request RegExp construction. Cache entries are released automatically when the config object itself rotates. Adds packages/api/src/middleware/messagePiiFilter.spec.ts covering the default-starter rejections, the starterPatterns subset and empty-array semantics, customPatterns matching layered on top of and in place of the starters, the no-config and empty-text pass-through paths, and a memoization regression check. * 🛡️ fix: Skip invalid customPattern regexes instead of crashing the request Admin DB overrides for `messagePiiFilter.customPatterns` reach `req.config` via `mergeConfigOverrides`, which deep-merges raw override values without re-running `configSchema`. A typo'd regex like `(` would slip past the YAML-load validation and throw inside `new RegExp(...)` during `compile()`, returning 500 for every chat request until the operator rolled the override back. Wrapped the per-pattern compile in a try/catch that logs the invalid pattern id + reason and skips it, so other valid patterns (starters and other custom entries) keep filtering. Added a regression test alongside the existing spec. * 🛡️ feat: Extend PII filter to OpenAI-compatible and Responses agent APIs The chat-route middleware operates on `req.body.text`, but the remote agent API endpoints (`/api/agents/v1/chat/completions`, `/api/agents/v1/responses`) accept the same prompt content as a `messages` array or an `input` field. A caller using their API key could send a credential-shaped value through either route and bypass the configured PII filter even though they share the same agent and model backbone the middleware is meant to guard. Factored out `findPiiMatchInMessages`, a tolerant walker that handles both `content: string` and `content: ContentPart[]` user-message shapes against the same compiled, cached pattern list. Wired it into the OpenAI-compat controller after agent lookup and into the Responses controller right after `convertToInternalMessages`. Each returns the endpoint's native 400 error shape (`sendErrorResponse` / `sendResponsesErrorResponse`) with the `message_pii_filter_block` code when a user message matches. * 🩹 test: Add findPiiMatchInMessages to OpenAI + Responses controller mocks The OpenAI-compat and Responses controller specs mock `@librechat/api` with a hand-listed object. The new `findPiiMatchInMessages` export wired into both controllers in 3ea35af was missing from those mocks, so the production lookup returned undefined and the controllers threw at request time under jest. Added the missing entries (default mock: returns null so the handlers fall through to the existing happy paths). All 278 agents-controller tests pass locally. * 🧹 refactor: Namespace messagePiiFilter under messageFilter.pii + fix import order Renames the yaml field `messagePiiFilter` to `messageFilter.pii`, the module to `messageFilterPii`, the factory to `createMessageFilterPii`, the type to `MessageFilterPiiConfig`, and the error code to `message_filter_pii_block`. The wrapper `messageFilter` namespace gives future safety filters (e.g. `messageFilter.toxicity`) a place to plug in without restructuring the config later. The `findPiiMatchInMessages` helper kept its name because it already describes what it does at the value level. Also fixes import order Danny flagged on the OpenAI-compatible and Responses controllers: `findPiiMatchInMessages` was appended at the bottom of two `require('@librechat/api')` destructures rather than placed in the length-sorted slot the house style expects. * 🧹 chore: Length-sort the general require destructure in responses.js Reorders the general sub-group inside the `require('@librechat/api')` destructure shortest to longest so the whole block conforms to the length-sort rule the file's `// Responses API` sub-group already follows. Pure reorder, no other changes. * 🧹 chore: Length-sort the defaultConfig block in AppService Reorders the `defaultConfig` keys in `packages/data-schemas/src/app/service.ts` shortest-line to longest-line, with the explicit-value entries (`mcpConfig`, `fileStrategies`, `cloudfront`) trailing the shorthand ones. Pure reorder, no behavior change.
Summary
Adds an opt-in PII filter that scans user prompts for credential-shaped patterns and rejects the request with HTTP 400 + the
message_filter_pii_blockcode when one matches. A matched credential never reaches OpenAI moderation, the model, the database, or any other downstream consumer. Three starter patterns ship by default (sk-prefix tokens,Bearerheaders,api-key:headers); operators can subset them withstarterPatterns: [...]or supply their own regex viacustomPatterns. Omit the section entirely to leave the filter inert.The yaml config lives under
messageFilter.piirather than a flatmessagePiiFilter, so future safety filters (e.g.messageFilter.toxicity) can plug in alongside without restructuring the schema later.The same pattern set guards three entry points so a caller cannot bypass the filter by switching protocols:
/api/agents/chat— an Express middleware mounted ahead ofmoderateText./api/agents/v1/chat/completions— the OpenAI-compatible controller checksrequest.messagesafter the agent lookup./api/agents/v1/responses— the Responses controller checks the converted internal messages just afterconvertToInternalMessages.Each entry point returns the 400 in its endpoint-native error shape (
{ error, message }for the chat route, the OpenAI/Responses error envelopes for the remote APIs), all carrying the samemessage_filter_pii_blockcode so callers can branch on a single code.Implementation lives in
packages/api/src/middleware/messageFilterPii.ts: a WeakMap-memoized compile of starter + custom patterns, plus two consumers — the chat-routecreateMessageFilterPiimiddleware factory and afindPiiMatchInMessageshelper that handles bothcontent: stringandcontent: ContentPart[]shapes used by the remote APIs. Schema lives inpackages/data-provider/src/config.ts(messageFilterSchema = z.object({ pii: messageFilterPiiSchema.optional() })). The route mount is inapi/server/routes/agents/chat.js; the controller hooks are inapi/server/controllers/agents/openai.jsandresponses.js.Change Type
Testing
Screen.Recording.2026-06-08.at.11.17.01.AM.mov
Test Configuration:
Add
messageFilter: { pii: {} }tolibrechat.yaml(or the example block fromlibrechat.example.yamlfor explicit patterns). Run backend + frontend, send chat messages containing credential-shaped text, or call the remote APIs with an agent API key (Authorization: Bearer <key>).Automated:
cd packages/api && npx jest src/middleware/messageFilterPii.spec.ts— 20 unit cases covering middleware behavior, the messages-array helper for both content shapes,starterPatterns: []semantics,customPatternsmatching alongside and in place of starters, the empty-text/no-config pass-throughs, memoization, and the invalid-regex override fallback.cd api && npx jest server/controllers/agents/— 278 controller tests pass with the new mocks forfindPiiMatchInMessages.Manual end-to-end against a running backend (verified):
my key is sk-proj-FAKE1234567890ABCDEFvia the chat UI is rejected with 400 +message_filter_pii_blockPOST /api/agents/v1/chat/completionswith a normal user message returns 200 with the agent replyPOST /api/agents/v1/chat/completionswithsk-in the user content returns 400 in the OpenAI error envelope withcode: "message_filter_pii_block"POST /api/agents/v1/responseswith normalinputreturns 200POST /api/agents/v1/responseswithsk-ininputreturns 400 with the same code in the Responses error envelopeChecklist