Service boundary¶
Outside the generated bindings/ package, service/ is the only layer that
imports gRPC or generated protobuf bindings. It translates between OpenShell's
transport contract and Privacy Guard's one-text domain contract.
The .proto file and generated bindings checked into this project are copied
from OpenShell. Protocol changes must land upstream and be adopted from a new
canonical copy; Privacy Guard must not create a private fork.
gRPC methods¶
Privacy Guard implements the three methods currently defined by
SupervisorMiddleware:
| RPC | Behavior |
|---|---|
Describe |
Advertises service identity, one pre-credentials HTTP binding, and the 4 MiB body limit |
ValidateConfig |
Validates supplied policy configuration and registered resources |
EvaluateHttpRequest |
Validates transport input, resolves the active processor, processes text, and returns a decision |
Describe advertises only
SUPERVISOR_MIDDLEWARE_PHASE_PRE_CREDENTIALS. Evaluations at any other phase
are invalid input.
The current manifest message has no field for the finalized policy schema or
engine discovery metadata. Those are available through the local
privacy-guard configuration-schema and privacy-guard engines commands.
Configuration lifecycle¶
The current protocol carries a complete google.protobuf.Struct on both
validation and every request evaluation.
ValidateConfig:
- rejects an encoded
Structlarger than 64 KiB before conversion - converts the
Structto a mapping - validates the registry-built discriminated policy model
- validates each exact engine config against registered resources
- validates replacement support for a replace action
- returns
valid=true, or a content-safe reason
It does not construct engines, change the active policy, contact model providers, download resources, or write artifacts. Validation runs in the bounded worker pool so catalog parsing and engine-specific checks do not block the async gRPC event loop.
During evaluation, the service validates and normalizes the same config. It
retains one active pair: the complete immutable validated configuration and its
configured RequestProcessor. Equal config reuses the processor. When no
processor is active or config differs, one serialized update path constructs
engines from the exact stage configs and operator-injected resources, constructs
a candidate processor, and atomically replaces the active pair only after
preparation succeeds. Failure preserves the prior active pair and fails the
triggering evaluation.
Validation, resolution or preparation, strict UTF-8 decoding, and processing all run in the bounded worker pool. Validation still occurs for every evaluation. Equivalent normalized Regex catalogs may reuse an entry in the independent compiled-rule cache during validation and engine construction.
Regex's compiled-rule LRU retains at most 128 catalogs and 32 MiB. Each entry weighs its canonical catalog bytes plus 4 KiB per rule. Evicting an entry only removes the cache's reference. A processor that already uses those rules remains valid.
An otherwise valid compiled-rule entry larger than the byte budget is built and used for the current operation but is not retained in that cache. The active processor itself has no LRU admission budget: it is deliberate process state retained until replacement or shutdown. Regex cache eviction or a skipped entry never invalidates it. Process restart discards the active processor, and the first later evaluation reconstructs it from supplied configuration.
Each active processor receives the server's operational processing timeout.
The default is 1 second shared across every configured stage. Operators may set
up to 30 seconds with privacy-guard serve --timeout-seconds; programmatic
applications pass the same timeout_seconds argument to PrivacyGuardServer.
OpenShell's outer middleware timeout must include the processing timeout plus
additional headroom for worker queueing, repeated configuration validation,
and occasional candidate preparation so the supervisor does not end the RPC
first.
Incoming requests¶
For each evaluation, the service:
- validates the pre-credentials phase
- validates the encoded context (4 KiB), configuration (64 KiB), target (32 KiB), headers (128 entries and 64 KiB), and body (4 MiB) limits
- acquires one bounded worker slot
- converts transport-safe integral numbers, validates the maximum ten stages, and resolves or updates the active policy in that worker
- allows an empty body without invoking an engine
- decodes a non-empty body as strict UTF-8
- calls
RequestProcessor.process(text)in the same worker
Request context, target, headers, middleware name, and protobuf values remain at the service boundary. The request ID is used only for content-safe operational logging.
Outgoing results¶
The processor returns RequestProcessingResult. The service maps it to
HttpRequestResult:
| Domain result | Protobuf result |
|---|---|
| Detect allow | DECISION_ALLOW, has_body=false |
| Block allow with no detections | DECISION_ALLOW, has_body=false |
| Replace allow | DECISION_ALLOW, has_body=true, final UTF-8 body |
| Policy block | DECISION_DENY, privacy_guard_blocked, no body |
| Limit deny | DECISION_DENY, privacy_guard_limit_exceeded, no body or partial findings |
The service checks the encoded replacement size again before serialization.
Findings¶
The processor has already aggregated occurrences by source stage, entity, and
confidence. For each EntityDetectionSummary, the service emits:
type:detected_entitylabel:entity (source-stage)confidence: the categorical value or empty when absentcount: aggregate occurrence count
The current OpenShell Finding message has no dedicated source field.
Stage provenance is therefore secondary text in the bounded label while the
entity remains primary. Matched content, offsets, patterns, and raw engine
metadata never cross the protobuf boundary.
Concurrency model¶
The gRPC server accepts at most 16 concurrent RPCs. Synchronous processing uses a dedicated executor and semaphore with four active slots:
gRPC event loop
|
| acquire processing slot
v
4-slot semaphore
|
v
4-thread executor
|
v
configuration validation
and active-policy resolution
|
v
RequestProcessor.process
|
v
ordered engine pipeline
This keeps synchronous configuration and engine work off the async event loop and bounds the number of active operations.
The active processor, engine instances, and injected resources may be used by multiple worker threads. They must retain no mutable per-request state and must be safe for concurrent access. Policy preparation is serialized. A candidate is fully prepared before the active reference is swapped. An evaluation that already captured the old processor may finish with it while later evaluations use the replacement; this is an atomic reference cutover, not a draining cutover.
Cancellation¶
Cancelling an async RPC cannot stop Python code already running in its worker thread. The service shields the worker bridge and releases the semaphore slot only after that worker actually finishes.
Cancellation therefore cannot create more than four active synchronous operations. An engine should pass the remaining shared timeout to any delegated API that supports bounded execution. A non-preemptible call continues to occupy its slot until it exits.
During shutdown, the server stops gRPC and waits for active executor work.
Error mapping¶
PrivacyGuardError classifies each cataloged failure as invalid input or
internal failure:
| Error kind | gRPC status |
|---|---|
invalid_input |
INVALID_ARGUMENT |
internal |
INTERNAL |
This mapping applies to evaluation. ValidateConfig instead returns
valid=false with a content-safe reason.
Unexpected failures become unexpected_service_failure. The service does not
return caught collaborator messages or exception chains.
A gRPC failure is distinct from a successful policy deny. OpenShell applies the middleware registration's failure behavior when an RPC fails. A policy deny is a successful RPC result that explicitly stops the request.
Programmatic server lifecycle¶
PrivacyGuardServer is the high-level programmatic API. It owns:
The built-in registry always includes RegexEngine and includes NEREngine
only when the operator supplies explicit NER resources. Operators register
custom engines and other resource-backed tool integrations before registry
finalization, then pass that registry to PrivacyGuardServer.
The registry is an explicit application-scoped dependency, not a global
singleton. PrivacyGuardServer and PrivacyGuardMiddleware reject unfinalized
registries. A deployment creates and finalizes one registry during startup;
the middleware constructs configured stage engines and the active processor
from that registry.
Different middleware applications in the same process may intentionally use
different engine inventories or runtime resources.
Synchronous applications use:
from privacy_guard.engines.registry import create_builtin_registry
from privacy_guard.service import PrivacyGuardServer
server = PrivacyGuardServer(
create_builtin_registry(),
timeout_seconds=5,
)
server.serve_sync("127.0.0.1:50051")
Async applications call await server.serve_async(address) instead. The
explicit suffixes make the blocking behavior visible at each call site. Both
entry points use the same server instance and lifecycle.
The listen address uses host:port or bracketed IPv6 [address]:port form,
with an explicit port from 1 through 65535. Privacy Guard rejects zero and
out-of-range numeric ports before calling gRPC, then verifies that gRPC bound
the requested port before starting the service.
The server:
- creates an unstarted async gRPC server with receive and concurrency limits
- binds the configured address
- starts and waits for termination
- stops gRPC
- closes the middleware executor
A bind or asynchronous gRPC startup RuntimeError becomes the stable
server_bind_failed error. Its catalog operation is the broader server.start
phase so the rendered failure does not mislabel a post-bind startup error.
The server module has no command framework, module-import-string, discovery, schema-rendering, or command-logging responsibilities.
Command-line application¶
privacy_guard.cli owns the Typer application. The --registry-factory option
identifies a Python function in module:function form. At startup, the CLI
imports the module, calls the function once, and uses the returned finalized
EngineRegistry for the selected command:
privacy-guard --registry-factory my_engines:create_registry engines
privacy-guard --registry-factory my_engines:create_registry configuration-schema
privacy-guard --registry-factory my_engines:create_registry serve
This is a deployment setting, not part of an OpenShell policy. The person starting Privacy Guard chooses the function, and importing its module executes Python code, so it must refer to code that the deployer controls. A policy cannot select a factory or cause Privacy Guard to import a module.
Applications that start PrivacyGuardServer from Python do not need this CLI
option. They create the registry normally and pass it to the server:
The CLI exposes:
privacy-guard engines
privacy-guard configuration-schema
privacy-guard serve --listen 127.0.0.1:50051 --timeout-seconds 5
Entity behavior comes from policy configuration, not server startup flags. Engine implementations and operational resources come from the selected application registry. The processing timeout is an operational server bound, not policy-controlled behavior.
Upstream protocol work¶
The intended large-catalog and discovery experience requires coordinated OpenShell changes for:
- explicit preparation and activation that accepts expanded configuration larger than the current 64 KiB evaluation limit
- versioned policy references on evaluations
- finalized policy schema and engine discovery in the manifest
- a dedicated finding source field
These are protocol evolution items, not local implementation hooks. Until they land upstream, Privacy Guard continues to validate the per-evaluation config, treat a changed valid config as an active-policy update, expose discovery through the CLI, and render stage provenance inside the finding label.
The protocol cannot distinguish an intentional update from any other changed configuration. Each process must receive one consistent policy stream. Interleaved old and new configurations can repeatedly switch the active policy.
Testing the boundary¶
Service tests should cover:
- protobuf/domain translation
- manifest fields supported by the current protocol
- pure policy validation
- phase, body-size, and UTF-8 validation
- first activation, equal-config reuse, serialized replacement, failed-update preservation, and concurrent cutover
- detect, replacement, block, and limit serialization
- finding encoding and limits
- gRPC status mapping
- worker-slot behavior under cancellation
- startup, bind failure, and shutdown
Engine algorithms and ordered policy semantics belong in engine and processor tests unless transport translation is essential to the case.