Data Flow Diagrams
Detailed data flow diagrams for every major OGuardAI operation, showing where raw PII exists and what crosses the trust boundary
This document describes the data flow for every major operation in OGuardAI. Each diagram shows where raw PII exists (Trusted Zone only) and what data crosses the trust boundary.
1. Transform Flow
The transform flow takes raw user input, detects and replaces sensitive entities with semantic tokens, and returns safe text with an encrypted session blob.
Client Request
| { input: "Contact erika.mustermann@example.com", policy: "default" }
|
v
+---------------------------------------------------------------+
| TRUSTED ZONE |
| |
| +----------+ Request validated, API key / JWT checked |
| | Auth |--> Scopes verified (requires: Transform scope) |
| +----+-----+ |
| | |
| v |
| +------------------+ injection patterns scanned |
| | Prompt Security |--> Action: Allow / Strip / Block |
| +----+-------------+ |
| | |
| v |
| +------------------+ Builtin regex (30+ patterns) |
| | Detect Entities | + Python NER sidecar (optional) |
| | |--> Entities: [{email, erika.mustermann@example.com, |
| | | span: {8,25}, confidence: 0.95}] |
| +----+-------------+ |
| | |
| v |
| +------------------+ Deterministic ID assignment |
| | Tokenize | Sort by (start, end, type, value) |
| | |--> erika.mustermann@example.com -> `{{email:e_001:7b1068662ab5aa11bb22cc33dd44ee55}}` |
| +----+-------------+ |
| | |
| v |
| +------------------+ Per-entity-type action lookup |
| | Policy Evaluate | each type -> its policy action: |
| | | tokenize / redact / abstract |
| | | (whitelisted values pass through) |
| +----+-------------+ |
| | |
| v |
| +------------------+ Replace entities in text |
| | Transform |--> "Contact `{{email:e_001:7b1068662ab5aa11bb22cc33dd44ee55}}`" |
| +----+-------------+ |
| | |
| v |
| +------------------+ AES-256-GCM encrypt token map |
| | Seal Session | 12-byte random nonce, 16-byte tag |
| | |--> Encrypted blob with tenant + expiry |
| +----+-------------+ |
| | |
+-------+--------------------------------------------------------+
|
v
Client Response
{
safe_text: "Contact `{{email:e_001:7b1068662ab5aa11bb22cc33dd44ee55}}`",
session_id: "abc-123",
session_state: "<encrypted-blob>",
entities: [{token: "`{{email:e_001:7b1068662ab5aa11bb22cc33dd44ee55}}`", type: "email", ...}],
stats: {entities_detected: 1, entities_transformed: 1}
}What crosses the trust boundary: safe_text (tokenized), session_state (encrypted), entity metadata, and statistics. Tokenized entities carry no raw value. The exception is a value the active policy whitelists: that raw value is deliberately passed through into safe_text as a conscious policy decision.
Each span is a pair of UTF-8 byte offsets into the exact request text, with an exclusive end. The example above is ASCII, so the numbers happen to coincide with character positions; on non-ASCII input they do not. Slice the request bytes, or convert to your language's index unit first: Python indexes code points, JavaScript and Java index UTF-16 code units.
2. Rehydrate Flow
The rehydrate flow takes LLM output containing semantic tokens and restores the original values using the encrypted session mapping.
Client Request
| { output: "Reply to `{{email:e_001:7b1068662ab5aa11bb22cc33dd44ee55}}`", session_state: "<blob>",
| restore_mode: "full", output_channel: "customer_email" }
|
v
+---------------------------------------------------------------+
| TRUSTED ZONE |
| |
| +----------+ Request validated |
| | Auth |--> Scopes verified (requires: Rehydrate scope) |
| +----+-----+ |
| | |
| v |
| +------------------+ Verify GCM auth tag |
| | Unseal Session | Check tenant ID matches caller |
| | | Check TTL not expired |
| | |--> Token map: {e_001: erika.mustermann@example.com} |
| +----+-------------+ |
| | |
| v |
| +------------------+ 3-stage repair pipeline |
| | Token Repair | 1. Strict parse |
| | | 2. Format repair (fix braces, case) |
| | | 3. Fuzzy resolve (single-candidate) |
| +----+-------------+ |
| | |
| v |
| +------------------+ Apply restore mode per entity type |
| | Rehydrate | full: exact value |
| | | partial: j***@*** |
| | | masked: j*******************e |
| | | formatted: person/company only |
| | | abstract: (email on file) |
| | | none: [REDACTED] |
| | |--> "Reply to erika.mustermann@example.com" |
| +----+-------------+ |
| | |
| v |
| +------------------+ Re-scan for LLM-hallucinated PII |
| | Output Guard | (optional, configurable) |
| | |--> Action: Redact / Tokenize / Abstract |
| +----+-------------+ |
| | |
+-------+--------------------------------------------------------+
|
v
Client Response
{
restored_text: "Reply to erika.mustermann@example.com",
tokens_resolved: 1,
tokens_unresolved: [],
stats: {rehydrate_time_ms: 0.5}
}What crosses the trust boundary (inbound): Tokenized text (no raw PII) and encrypted session blob. What crosses the trust boundary (outbound): Restored text (raw PII returns to the authorized caller only).
3. RAG Flow (Retrieval-Augmented Generation)
RAG workflows have four distinct stages, each with its own protection requirements.
STAGE 1: DOCUMENT INGESTION
===========================
Raw Document
| "Erika Mustermann (erika.mustermann@example.com) filed complaint #C-789..."
|
v
+-------------------------------------------+
| TRUSTED ZONE |
| |
| Chunk document (paragraph boundaries) |
| | |
| v |
| Detect + Tokenize per chunk |
| (cross-chunk entity identity preserved) |
| | |
| v |
| Seal session per document |
| (store blob alongside chunk metadata) |
+-------+-----------------------------------+
|
v
Vector Store (UNTRUSTED)
Chunk: "`{{person:p_001:4c0d9a2f81beaa11bb22cc33dd44ee55}}` (`{{email:e_001:d24e0b7c95aaaa11bb22cc33dd44ee55}}`) filed complaint #C-789..."
Session blob: <encrypted>
STAGE 2: QUERY TRANSFORM
========================
User Query
| "What happened with Erika Mustermann's complaint?"
|
v
+-------------------------------------------+
| TRUSTED ZONE |
| |
| Detect + Tokenize query |
| | |
| v |
| "What happened with |
| `{{person:p_001:4c0d9a2f81beaa11bb22cc33dd44ee55}}`'s complaint?" |
+-------+-----------------------------------+
|
v
Vector DB Search (UNTRUSTED)
Query: "What happened with `{{person:p_001:4c0d9a2f81beaa11bb22cc33dd44ee55}}`'s complaint?"
Results: matching chunks (all tokenized)
STAGE 3: CONTEXT ASSEMBLY
=========================
Retrieved Chunks (raw text as returned by the store)
|
v
+-------------------------------------------+
| TRUSTED ZONE |
| |
| Detect + tokenize every submitted chunk. |
| The endpoint re-runs detection rather |
| than trusting the store: a chunk that |
| arrives with raw PII in it is protected |
| here, not merely inspected |
| | |
| v |
| Optional access_level filter: a chunk |
| above the requested classification is |
| dropped; a missing label ranks |
| most-restrictive (fail closed) |
| | |
| v |
| Merge tokens into the query session and |
| assemble context for the LLM prompt |
+-------+-----------------------------------+
|
v
LLM (UNTRUSTED)
Prompt: system preamble + tokenized query + tokenized context
STAGE 4: ANSWER REHYDRATION
===========================
LLM Response (tokenized)
| "`{{person:p_001:4c0d9a2f81beaa11bb22cc33dd44ee55}}` filed complaint #C-789 on 2024-01-15..."
|
v
+-------------------------------------------+
| TRUSTED ZONE |
| |
| Token repair (3-stage) |
| | |
| v |
| Rehydrate (restore mode per policy) |
| | |
| v |
| Output guard (scan for hallucinated PII) |
+-------+-----------------------------------+
|
v
Final Answer
"Erika Mustermann filed complaint #C-789 on 2024-01-15..."4. Proxy Flow
The proxy mode intercepts calls to the configured LLM provider API, transparently applying protection to detected values.
Application
| POST /v1/chat/completions
| { messages: [{role: "user", content: "Email erika.mustermann@example.com"}] }
|
v
+---------------------------------------------------------------+
| TRUSTED ZONE |
| (OGuardAI Proxy) |
| |
| +---------------+ |
| | Intercept | Parse incoming request |
| +-------+-------+ |
| | |
| v |
| +---------------+ Detect + tokenize message content |
| | Transform | Transforms user, assistant, system, and |
| | Messages | developer messages; only empty roles skip |
| +-------+-------+ |
| | |
| v |
| +---------------+ |
| | Forward |--> Upstream LLM API (OpenAI, Anthropic, |
| | Request | Azure, Vertex, Mistral, local model) |
| +-------+-------+ |
| | |
| v |
| +---------------+ LLM response received |
| | Receive | (contains only tokenized content) |
| | Response | |
| +-------+-------+ |
| | |
| v |
| +---------------+ Repair + rehydrate tokens |
| | Rehydrate | Apply output channel restore mode |
| | Response | |
| +-------+-------+ |
| | |
+----------+----------------------------------------------------+
|
v
Application
{ choices: [{message: {content: "Email erika.mustermann@example.com"}}] }Key property: The application code does not need to change. The proxy acts as a transparent intermediary for the LLM provider URL.
5. Streaming Flow (SSE)
Streaming runs the full pipeline first, then delivers the finished text as Server-Sent Events. The server does not rehydrate or tokenize incrementally as bytes arrive: it computes the complete result, then slices it into fixed-size SSE chunks. Because a {{type:id:cap}} token can straddle a chunk boundary, the server holds a trailing partial token back until the boundary is safe. This is output chunking of an already-computed result, not token-by-token generation.
Client Request
| POST /v1/transform/stream
| { input: "Contact erika.mustermann@example.com about order #ORD-456" }
|
v
+---------------------------------------------------------------+
| TRUSTED ZONE |
| |
| Auth --> Prompt Security --> Detect --> Tokenize --> Seal |
| |
| Transform produces safe_text: |
| "Contact `{{email:e_001:9f31c47d20e8aa11bb22cc33dd44ee55}}` about order `{{order:o_001:5a6de1f30c92aa11bb22cc33dd44ee55}}`" |
| |
| +--------------------------+ |
| | SSE Event Generator | |
| | Chunk 1: "Contact " |--> SSE: data: {"chunk": ...} |
| | Chunk 2: "`{{email:e_001:9f31c47d20e8aa11bb22cc33dd44ee55}}`|--> SSE: data: {"chunk": ...} |
| | Chunk 3: " about order " |--> SSE: data: {"chunk": ...} |
| | Chunk 4: "`{{order:o_001:5a6de1f30c92aa11bb22cc33dd44ee55}}`|--> SSE: data: {"chunk": ...} |
| | Final frame |--> SSE: data: {"session_state": ..., |
| | | "session_id": ..., "stats": ..., |
| | | "complete": true} |
| +--------------------------+ |
| |
+---------------------------------------------------------------+
Client Request (Streaming Rehydrate)
| POST /v1/rehydrate/stream
| { output: "Reply to {{email:e_001:9f31c47d20e8aa11bb22cc33dd44ee55}} today", session_state: "<blob>" }
| (the field is `output`: rehydrate's input is the LLM's output. It carries the
| complete token-bearing text, e.g. a fully buffered LLM response)
|
v
+---------------------------------------------------------------+
| TRUSTED ZONE |
| |
| Unseal session --> token map ready |
| |
| Rehydrate the FULL input in one pass: |
| "Reply to {{email:e_001:9f31c47d20e8aa11bb22cc33dd44ee55}} today" |
| --> restored_text: "Reply to erika.mustermann@example.com today" |
| |
| SSE Event Generator + TokenBoundaryBuffer: |
| slice restored_text into fixed-size chunks; hold a |
| trailing partial token until the boundary is safe, |
| so no SSE frame ever ends mid-token. |
| |
| Each chunk --> SSE output event |
| |
+---------------------------------------------------------------+
|
v
Client receives SSE stream with rehydrated contentKey property: streaming does not rehydrate incrementally as bytes arrive. The rehydrate/stream request carries the complete token-bearing text (for example a fully buffered LLM response); the server rehydrates it in one pass, then the TokenBoundaryBuffer slices the finished text into SSE frames. It ensures a {{type:id:cap}} token (a token in transform's safe_text, or an unresolved token that rehydrate left as-is) is never split across two frames: a trailing partial token is held until the next slice completes it, so no partial token is emitted to the client.
6. File / Document Processing Flow
File upload processing for documents (PDF, DOCX, TXT, CSV, HTML, Markdown).
Client Request
| POST /v1/transform/file
| Content-Type: multipart/form-data
| file: contract.pdf
|
v
+---------------------------------------------------------------+
| TRUSTED ZONE |
| |
| +---------------+ |
| | File Ingest | Extract text from the document. No |
| | | network call and no third-party binary. |
| | | A PDF is parsed in a re-executed child |
| | | of this same image, so a parser fault |
| | | cannot take the server down. It is read |
| | | from the embedded text layer only, so a |
| | | scanned page yields no text |
| +-------+-------+ |
| | |
| v |
| +---------------+ Standard transform over the WHOLE |
| | Transform | extracted text in one pass. The document |
| | (single pass) | is not chunked here; the same value |
| | | anywhere in it gets the same token |
| +-------+-------+ |
| | |
| v |
| +---------------+ |
| | Seal Session | Single session for the entire document |
| +-------+-------+ |
| | |
+----------+----------------------------------------------------+
|
v
Client Response
{
safe_text: "<entire document with tokens>",
entities: [{token: "...", type: "person", ...}],
session_id: "abc-123",
session_state: "<encrypted-blob>",
original_format: "Pdf",
original_size: 84213,
stats: {entities_detected: 8, entities_transformed: 8}
}Chunking belongs to the RAG ingest flow (section 3), not to this endpoint. A failed extraction is
classified rather than uniformly blamed on the upload: 400 for an unsupported or malformed document,
413 when a configured safety ceiling refused it, and 503 when the runtime could not perform the
extraction at all, which is the retriable case. A PDF sandbox that cannot be spawned is that third
case: the document was never examined, so it is not implicated.
7. Image Processing Flow (OCR)
Image processing with optional OCR text extraction.
Client Request
| POST /v1/transform/image
| Content-Type: multipart/form-data
| file: business_card.png
|
v
+---------------------------------------------------------------+
| TRUSTED ZONE |
| |
| +---------------+ Tesseract OCR (optional module) |
| | OCR Extract | Random temp file name (RAII cleanup) |
| | |--> Extracted text: "Erika Mustermann |
| | | +49 30 12345678 erika.mustermann@example.com" |
| +-------+-------+ |
| | |
| v |
| +---------------+ |
| | Detect + | Standard detection pipeline |
| | Tokenize | |
| +-------+-------+ |
| | |
| v |
| +---------------+ |
| | Transform + | |
| | Seal | |
| +-------+-------+ |
| | |
+----------+----------------------------------------------------+
|
v
Client Response
{
safe_text: "{{person:p_001:b8e02d5c714faa11bb22cc33dd44ee55}} {{phone:ph_001:36c9a05df4e1aa11bb22cc33dd44ee55}} {{email:e_001:e5170b28c9adaa11bb22cc33dd44ee55}}",
entities: [...],
bounding_boxes: [...],
session_state: "<encrypted-blob>"
}Security note: OCR temp files use random file names and are cleaned up via RAII (Drop trait). No raw content persists on disk after the request completes.
8. Batch Processing Flow
Batch API for processing multiple inputs in a single request.
Client Request
| POST /v1/batch/transform
| { items: [
| {text: "Email: erika.mustermann@example.com"},
| {text: "Call Max Mustermann at +1-555-0123"},
| {text: "IBAN: DE97990000000123456789"}
| ]}
|
v
+---------------------------------------------------------------+
| TRUSTED ZONE |
| |
| +----------------------------------------------+ |
| | For each item (parallel where possible): | |
| | 1. Detect entities | |
| | 2. Tokenize | |
| | 3. Policy evaluate | |
| | 4. Transform | |
| +-------+--------------------------------------+ |
| | |
| v |
| +---------------+ Merge all token maps |
| | Seal Session | Each item sealed independently (default) |
| +-------+-------+ |
| | |
+----------+----------------------------------------------------+
|
v
Client Response
{
results: [
{index: 0, safe_text: "Email: {{email:e_001:08f4b6d1a273aa11bb22cc33dd44ee55}}", session_state: "<per-item-blob>",
entities_detected: 1, entities_transformed: 1},
{index: 1, safe_text: "Call {{person:p_001:c72e19f05b84aa11bb22cc33dd44ee55}} at {{phone:ph_001:41d8a97e63cfaa11bb22cc33dd44ee55}}",
session_state: "<per-item-blob>", entities_detected: 2, entities_transformed: 2},
{index: 2, safe_text: "IBAN: {{iban:ib_001:f0b35c8a92d6aa11bb22cc33dd44ee55}}", session_state: "<per-item-blob>",
entities_detected: 1, entities_transformed: 1}
],
stats: {total_items: 3, total_entities: 4}
}A batch result reports counts, not a diagnostic entity list: entities_detected and
entities_transformed, keyed by the request-order index. An item that fails carries an error string
and an empty safe_text while the rest of the batch still processes.
Data Classification Summary
| Data Type | Where It Exists | Encryption |
|---|---|---|
| Raw PII (original values) | Trusted Zone in-memory during request, plus any entity a policy explicitly allows through to the destination | Designed to prevent unencrypted persistence |
| Token map (token ID to value) | Sealed session blob | AES-256-GCM |
| Tokenized text | Untrusted Zone (LLMs, vector stores, logs) | Not encrypted (contains no PII) |
| Entity metadata (type, gender, formality) | Untrusted Zone (entity_context) | Not encrypted (contains no PII) |
| Session blob | Client-carried (sealed) or in-process (memory) | AES-256-GCM (sealed) |
| Audit events | Log output | No PII present; encryption per SIEM policy |