Safety and limits¶
Privacy Guard bounds request text, engine output, detections, regex execution, transport output, and concurrency. Limits are part of the architecture, not optional tuning defaults.
Package-wide values live in constants.py.
Validation ownership¶
Each layer validates facts it can establish:
| Layer | Validates |
|---|---|
| Policy and engine config models | Strict types, exact fields, discriminators, local field rules, catalog bounds, and regex validity |
EngineRegistry |
Registration contracts, exact config and resource types, pure resource-backed config checks, and action/replacement compatibility |
EntityProcessingEngine.run() |
Text and invocation types, supported strategy, timeout, result model, spans, detection-only immutability, mutation attribution, and stage output bounds |
| Concrete engine | Algorithm-specific execution, replacement completeness, backend timeout propagation, and native failure normalization |
RequestProcessor |
Request text bounds, shared timeout, intermediate output, request-wide detection count, aggregation, and policy disposition |
| Service | Phase, transport body size, strict UTF-8, protobuf conversion, replacement encoding, finding representation, and response limits |
Keep request-wide policy decisions in the processor and algorithm-specific behavior in the concrete engine. The service adapts transport but does not reimplement either.
Text, processing, and transport limits¶
| Limit | Constant | Value | Owner |
|---|---|---|---|
| Incoming request body | MAX_BODY_BYTES |
4 MiB | Service |
| Input or intermediate UTF-8 bytes | MAX_BODY_BYTES |
4 MiB | Processor |
| Engine output UTF-8 bytes | MAX_BODY_BYTES |
4 MiB | Engine wrapper |
| Encoded replacement body | MAX_BODY_BYTES |
4 MiB | Service |
| gRPC receive message | MAX_RECEIVE_MESSAGE_BYTES |
5 MiB | gRPC server |
| Detections returned by one stage | MAX_DETECTIONS_PER_STAGE |
256 | Engine wrapper |
| Detections across one request | MAX_DETECTIONS_PER_REQUEST |
4,096 | Processor |
| Default shared timeout | DEFAULT_TIMEOUT_SECONDS |
1 second | Server and processor |
| Maximum configured timeout | MAX_TIMEOUT_SECONDS |
30 seconds | Server, processor, and Timeout |
| Active processor workers | MAX_CONCURRENT_PROCESSING |
4 | Service |
| Concurrent gRPC calls | MAX_CONCURRENT_RPCS |
16 | gRPC server |
| NER endpoint response | MAX_NER_ENDPOINT_RESPONSE_BYTES |
1 MiB | NER endpoint facade |
The gRPC receive allowance is 1 MiB larger than the request body limit for the protobuf envelope. The service still enforces the advertised 4 MiB body bound.
One monotonic Timeout is shared by all stages and final result validation.
RegexEngine passes the remaining duration into every backend search.
Third-party calls that accept a timeout should receive the same remaining
duration; calls that cannot be interrupted continue to occupy a service worker
until they exit.
Operators configure the internal bound with --timeout-seconds or the
programmatic PrivacyGuardServer(timeout_seconds=...) argument. OpenShell's
outer middleware timeout must include additional headroom for worker queueing
and configuration or engine preparation beyond this internal bound.
Diagnostic and result limits¶
| Limit | Constant | Value |
|---|---|---|
| Bounded diagnostic string | MAX_DIAGNOSTIC_TEXT_BYTES |
1,024 UTF-8 bytes |
| Metadata entries per detection | MAX_FINDING_METADATA_ENTRIES |
32 |
| Aggregated protobuf finding groups | MAX_PROTO_FINDING_GROUPS |
32 |
| Encoded size per protobuf finding | MAX_PROTO_FINDING_BYTES |
4 KiB |
The diagnostic-string bound applies to stage names, entity names, metadata keys and values, model-profile names, and other audit-safe identifiers built from the shared domain field type. These values must be non-empty printable Unicode without control or bidirectional formatting characters. Untrusted request IDs use the same content and size rules for logging; an invalid request ID is represented by a constant placeholder and does not change the evaluation result. The human-readable log message quotes request IDs and escapes ASCII spaces so their text cannot introduce another key/value token; structured log records retain the exact validated ID. Regex entity and supplied rule names have a stricter ASCII grammar and limit described below.
The processor aggregates occurrences before the service applies protobuf limits. If a safe result cannot be represented, the service returns a limit deny with no partial findings.
Regex and catalog limits¶
| Limit | Constant | Value |
|---|---|---|
| Regex entity or supplied rule name | MAX_REGEX_NAME_BYTES |
128 ASCII bytes |
| Entities per catalog | MAX_REGEX_ENTITIES_PER_CATALOG |
2,000 |
| Rules per catalog | MAX_REGEX_RULES_PER_CATALOG |
10,000 |
| Pattern string | MAX_REGEX_PATTERN_BYTES |
16 KiB |
Entity and rule names use
[A-Za-z_][A-Za-z0-9_-]*. Rule names are optional; their deterministic
derived diagnostic identities are not serialized back into configuration.
The per-evaluation config Struct is limited to 64 KiB. A catalog may satisfy
Privacy Guard's engine limits yet remain too large for the current upstream
protocol. Internal caching does not raise that transport ceiling.
NER configuration and execution limits¶
| Limit | Constant | Value |
|---|---|---|
| Labels per NER stage | MAX_NER_LABELS |
128 |
| UTF-8 bytes per label | MAX_NER_LABEL_BYTES |
256 |
| Total UTF-8 label bytes | MAX_NER_LABELS_BYTES |
16 KiB |
| Endpoint response bytes | MAX_NER_ENDPOINT_RESPONSE_BYTES |
1 MiB |
NER labels must be non-empty printable Unicode without leading or trailing whitespace and must be unique after Unicode case folding. Model identity, endpoint, and execution chunking are deployment resources rather than policy fields.
The endpoint facade bounds response bytes before JSON decoding and performs one request without retry or fallback. The local facade processes every supported input in overlapping code-point windows and rebases returned offsets. Its overlap must be smaller than its chunk length. Operators should choose overlap using the longest entity spans expected in the deployment corpus.
Both facades return only normalized label, start, end, and score values. The engine rejects unknown labels, invalid or out-of-bounds spans, non-finite or out-of-range scores, and excessive result cardinality. Request text, returned matched text, raw response bodies, endpoint URLs, credentials, and collaborator exception messages are not logged or included in errors.
Regex execution safety¶
RegexEngine validates and compiles the complete catalog before accepting
configuration. It:
- rejects invalid and empty pattern strings
- rejects patterns that match empty input
- rejects user-defined named groups and inline flags
- verifies its private trailing marker on every match
- treats a context-dependent zero-width match as an atomic configuration failure
- evaluates patterns independently to retain overlaps
- uses the timeout-capable third-party
regexbackend - caps detections per stage
- projects exact UTF-8 replacement size before rendering
No timeout, limit, or pattern failure returns partial stage detections or mutated text.
When a limit is exceeded¶
The processor returns a successful deny with
privacy_guard_limit_exceeded and no partial findings or replacement when:
- the shared timeout expires
- an engine exceeds its detection or output limit
- intermediate text exceeds the UTF-8 byte limit
- aggregate request detections exceed the limit
The service returns the same bounded deny when:
- findings exceed the protobuf group or encoded-size limit
- replacement text cannot be encoded within the body limit
- a deny reason code is not safely representable
The processor emits a content-safe timeout or resource limit kind, and the
service adapter emits resource for representation limits. Neither log includes
request content or collaborator error text. The deny reason directs users to
check that log, then reduce the request or replacement size, simplify the
configured stages and rules, or increase --timeout-seconds or
PrivacyGuardServer(timeout_seconds=...) up to 30 seconds. When increasing the
processing timeout, users must give OpenShell's middleware timeout additional
headroom for worker queueing and configuration or engine preparation. These
options reduce bounded output pressure or provide appropriate time without
weakening the fail-closed behavior.
Malformed input and engine contract or execution failures instead abort the RPC with a cataloged error. OpenShell then applies the middleware registration's failure behavior.
The distinction is deliberate:
- an exhausted declared processing bound is a closed, successful deny
- invalid user input is an
INVALID_ARGUMENTRPC failure - an engine or middleware defect is an
INTERNALRPC failure
Error catalog¶
Production failures use PrivacyGuardError and a stable ErrorCode. Each
error spec defines:
- invalid-input or internal classification
- responsible component
- failed operation
- content-safe summary
- remediation hint
Successful policy and safety-limit denials use stable reason codes plus content-safe recovery guidance; they do not add domain fields solely for diagnostic logging. CLI validation failures name the rejected option and state the accepted form or next action. Cataloged server-startup failures print one content-safe message and exit nonzero rather than exposing a traceback. Internal engine and third-party failures remain concise because the owning boundary translates them before they reach users.
Caught extension and third-party exceptions are translated at their trust boundary. Raw exception messages and chains are not exposed. Configuration errors do not include rejected pattern strings, paths, credentials, provider endpoints, or request text.
Use a new catalog entry for a genuinely new externally observable failure. Do not return raw Pydantic, regex-backend, engine, or protobuf exceptions.
Logging¶
Default operational logs include:
- request ID
- evaluation duration
- allow or deny action
- aggregate finding count
- stable error code
- stage source and invocation strategy at debug level
They do not include:
- request or replacement text
- matched or surrounding text
- offsets
- patterns or catalog contents
- headers, targets, or credentials
- model endpoints
- arbitrary exception text
Sensitive domain fields use repr=False to reduce accidental exposure. This
does not replace safe exception handling.
--debug enables content-safe processing diagnostics.
--debug-log-content deliberately logs complete input and processed text and
emits a warning; use it only in a controlled development environment.
Extension requirements¶
Custom engines should:
- use the base constructor and public
run()wrapper - return the exact
TextProcessingResultcontract - return stable, declared entity identifiers rather than values derived from request text
- keep request data local to
_run() - keep initialized state immutable and make resources concurrency-safe
- pass the shared remaining timeout to delegated APIs when they support it
- fail the stage on native partial failure
- use
TextProcessingResult.from_detections()to bound lazy detection streams - add manual bounds only when a low-level collaborator allocates proportional output before Privacy Guard can consume it
- translate expected collaborator failures to the content-safe engine exception hierarchy
- avoid logging input or caught exception text
Privacy Guard keeps framework-controlled fields and identifiers that pass its shared validation content-safe for findings and operational logs. Operator-installed Python engine code is trusted deployment code: it can access request text and must honor the declared identifier and logging contract. The framework cannot make arbitrary trusted Python code non-exfiltrating, so operators must review and install custom engines with the same care as other code in the middleware process.
Do not add retries, fallback providers, persistent request artifacts, or extra validation without a concrete failure mode and a clear owning layer.
State and retention¶
Cross-request entity memory is not implemented. Privacy Guard retains one active validated policy and its immutable configured engine state, but never request text, detections, or replacement mappings. The active processor is deliberate process state, not an LRU entry: it remains until successful replacement or shutdown.
During an update, the fully prepared candidate may coexist with the active processor. Evaluations that already captured the prior processor may keep it alive until they finish. A failed candidate leaves active state unchanged.
Regex's supporting compiled-rule cache remains bounded:
| Cache | Weight budget | Entry cap | Entry weight |
|---|---|---|---|
| Compiled Regex rules | 32 MiB | 128 | canonical catalog bytes plus 4 KiB per rule |
The compiled-rule allowance accounts for retained backend state that is much larger than short pattern text. One entry that exceeds the budget remains valid and usable for the current operation but is not retained there. This does not invalidate an active or candidate processor holding those rules. The active policy is not rejected by cache admission; policy and schema limits bound its shape, while custom-engine prepared memory remains an operator responsibility. Regex cache skip and eviction records contain only the cache name and weights, plus eviction counts.
Changing a limit¶
A limit change may affect policy schema, processor behavior, engine contracts, protobuf serialization, tests, examples, and the OpenShell middleware registration.
Before changing a limit:
- identify every layer that enforces or advertises it
- update exact-boundary tests
- check the failure result immediately above the boundary
- run the complete project validation
- benchmark changes that affect regex compilation or request processing
- coordinate upstream when the copied OpenShell protocol is involved