Skip to content

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 regex backend
  • 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_ARGUMENT RPC failure
  • an engine or middleware defect is an INTERNAL RPC 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 TextProcessingResult contract
  • 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:

  1. identify every layer that enforces or advertises it
  2. update exact-boundary tests
  3. check the failure result immediately above the boundary
  4. run the complete project validation
  5. benchmark changes that affect regex compilation or request processing
  6. coordinate upstream when the copied OpenShell protocol is involved

Back to the architecture overview