OGuardAI
Getting Started

Demo

Run the end-to-end OGuardAI demo, one AI request protected across formats, languages, and industries

Availability: The registry, image and package installs below are private today and provided under a written agreement with Oronts. Request access at /request.

The examples/yc-demo/ example in the repository is a runnable, terminal-only demo. One AI request goes to a model and comes back, and the model never receives a single raw identifier. The same tokenized answer is then restored differently for three readers: the customer sees their own values, an internal system sees masked values, the audit log sees nothing. The demo then repeats the same protection across formats (text, JSON, CSV, PDF, OCR image), four languages (English, German, Arabic, Japanese), and seven verticals, printing the real per-step latency throughout.

This page embeds the actual example source so you can rebuild it without opening the repository.

Jump to: Run it · The demo client · Configuration and policy · Expected output

What the demo shows

   application request            OGuardAI runtime            model provider
  (name, email, card, ...)     (the trusted data boundary)      (OpenAI / Claude / local)

     RAW INPUT  --->  DETECT + TOKENIZE  --->  SAFE PROMPT  --->  MODEL
                                                                        |
     FINAL OUTPUT  <---  REHYDRATE (per policy + channel)  <---  RESPONSE

   restore per reader:   customer -> real     internal -> masked     audit log -> removed
  • The hero flow: raw request, safe prompt, model response, customer output, audit log, and the output guard masking identifiers the model invents (regex second pass).
  • Field-level protection of a JSON chatbot payload, a CSV export, a PDF discharge summary, and an OCR image.
  • Custom regex identifiers (a hospital MRN) and zero-shot NER types (a diagnosis, a medication).
  • Four languages: English, German, Arabic, Japanese. The regex engine catches structured identifiers (email, phone, IBAN, card, order, German tax id) in every one of them.
  • Seven verticals: customer support, chatbot, e-commerce, hospital, insurance, tax office, government.
  • Real wall-clock latency per step, in a benchmark table at the end.

All PII in the fixtures is fictional.

Prerequisites

  • A running OGuardAI runtime. The demo reads GUARDAI_URL (default http://localhost:3000).
  • needs-NER for people, organisations, and locations. The Rust regex engine catches structured identifiers (email, phone, IBAN, card, order, German tax id) with no sidecar. Names have no fixed shape, so they need the Python NER detector sidecar. Every scenario except the JSON chatbot payload carries a person name, so run the full stack with NER for the complete flow. Without NER the demo still protects every structured identifier, labels the name it could not tokenize, and stops before the model call so nothing raw is sent.
  • The OCR image scenario needs Tesseract on the server host (apt install tesseract-ocr tesseract-ocr-deu). Without it that one scenario is skipped with a note and everything else runs.
  • OPENAI_API_KEY is optional. With it, the demo drafts the reply with a real model. Without it, a deterministic offline draft is used and the round trip still completes on tokens.

Run it

Requires a running OGuardAI runtime. There are two ways.

Full stack with NER

Adds people and organisations on top of the regex identifiers, in every language, and runs the full hero flow. Uses oguardai-demo.yaml from the example folder (input NER on, dev auth, regex output guard on but output NER off).

# from the repo root

# 1. the NER detector on :9090 (GLiNER, ~30s to become ready), from the published image.
# --host 127.0.0.1 keeps the unauthenticated detector bound to loopback, reachable by the server on
# the same host but not exposed on any other interface.
docker run -d --rm --network host -e GUARDAI_DETECTOR_ALLOW_INSECURE=true \
  --name oguardai-detector ghcr.io/oronts/oguardai/detector-py:0.1.0 \
  --host 127.0.0.1 --port 9090

# 2. stage the demo policy set and start the server (published image) with the demo config.
# Staged under target/ rather than copied into policies/: the shipped set is pinned by
# crates/policy/tests/load_policies.rs, and adding a directory there fails that test. The demo
# policy SOURCE stays tracked in this folder, and its patterns are guarded by that same test.
mkdir -p target/demo-policies
cp -r policies/. target/demo-policies/
cp -r examples/yc-demo/policies/oguardai-demo target/demo-policies/
# The demo config sets policy.directory: ./target/demo-policies (resolved against the image WORKDIR
# /app), so mount the staged set there. It also binds 0.0.0.0 under dev auth, which needs the
# GUARDAI_ALLOW_DEV_NON_LOOPBACK opt-in to boot.
docker run -d --rm --network host \
  -v "$PWD/examples/yc-demo/oguardai-demo.yaml:/app/oguardai.yaml:ro" \
  -v "$PWD/target/demo-policies:/app/target/demo-policies:ro" \
  -e GUARDAI_SESSION_SECRET="$(openssl rand -base64 32)" \
  -e GUARDAI_ALLOW_DEV_NON_LOOPBACK=1 \
  --name oguardai-server-demo \
  ghcr.io/oronts/oguardai/server:0.1.0 --config /app/oguardai.yaml

# 3. run the demo
cd examples/yc-demo && pip install -r requirements.txt
export GUARDAI_URL=http://localhost:3000
export OPENAI_API_KEY="sk-..."        # optional: draft with a real model, else an offline draft
python run.py

Rust-only preview

The built-in Rust regex detectors run with no sidecar and no Docker. Structured identifiers are protected in every language. Names need the NER detector, so the demo shows exactly what NER would add and never sends a raw value to a model.

A builtin-only server does not start on the full shipped policy set: most policies declare detection.required_for, which fails closed without NER. Stage only the policies that run without it.

# from the repo root: a builtin-safe policy set
mkdir -p target/builtin-policies
cp -r policies/default policies/csv-columns-demo policies/topical-demo target/builtin-policies/
cp -r examples/yc-demo/policies/oguardai-demo target/builtin-policies/

# copy examples/yc-demo/oguardai-demo.yaml, set detector.mode: builtin and
# policy.directory: ./target/builtin-policies, start the server with that config, then:
cd examples/yc-demo
export GUARDAI_URL=http://localhost:3000
python run.py

Every scenario keeps its own policy, and one whose policy is not loaded (the hospital PDF uses healthcare-clinical, which needs NER) is skipped with a note rather than run under the wrong rules.

The demo client

guard.py is the timed OGuardAI HTTP client. Every call returns its wall-clock latency so the demo can show the real overhead the runtime adds. This is the complete file.

"""Thin, timed OGuardAI client for the demo. Every call returns its wall-clock latency so the demo
can show the real overhead the runtime adds. Raw HTTP on purpose, so every request is visible."""

from __future__ import annotations

import os
import time
from dataclasses import dataclass, field
from typing import Any

import requests

URL = (os.getenv("GUARDAI_URL") or "http://localhost:3000").rstrip("/")
API_KEY = os.getenv("GUARDAI_API_KEY", "")
TIMEOUT = float(os.getenv("GUARDAI_TIMEOUT", "60"))


class GuardError(RuntimeError):
    def __init__(self, code: str, message: str) -> None:
        super().__init__(f"[{code}] {message}")
        self.code = code


@dataclass
class Timed:
    """A response plus the milliseconds it took."""

    data: dict[str, Any]
    ms: float


@dataclass
class Bench:
    """Accumulates per-step latencies for the closing summary table."""

    rows: list[tuple[str, str, float]] = field(default_factory=list)

    def add(self, scenario: str, step: str, ms: float) -> None:
        self.rows.append((scenario, step, ms))


def _headers() -> dict[str, str]:
    h = {"Content-Type": "application/json"}
    if API_KEY:
        h["X-API-Key"] = API_KEY
    return h


def _raise_for_guard(resp: requests.Response) -> None:
    if resp.status_code < 400:
        return
    body = resp.json() if resp.headers.get("content-type", "").startswith("application/json") else {}
    # Never surface the upstream body: it can echo the request's PII. Trust only the structured error.
    error = body.get("error")
    message = error if isinstance(error, str) else f"OGuardAI request failed (HTTP {resp.status_code})"
    raise GuardError(body.get("code", f"HTTP_{resp.status_code}"), message)


def _post_json(path: str, payload: dict) -> Timed:
    t0 = time.perf_counter()
    resp = requests.post(f"{URL}{path}", json=payload, headers=_headers(), timeout=TIMEOUT)
    ms = (time.perf_counter() - t0) * 1000.0
    _raise_for_guard(resp)
    return Timed(resp.json(), ms)


def _post_multipart(path: str, files: dict, data: dict) -> Timed:
    headers = {"X-API-Key": API_KEY} if API_KEY else {}
    t0 = time.perf_counter()
    resp = requests.post(f"{URL}{path}", files=files, data=data, headers=headers, timeout=TIMEOUT)
    ms = (time.perf_counter() - t0) * 1000.0
    _raise_for_guard(resp)
    return Timed(resp.json(), ms)


def capabilities() -> dict:
    resp = requests.get(f"{URL}/v1/capabilities", headers=_headers(), timeout=TIMEOUT)
    _raise_for_guard(resp)
    return resp.json()


def detector_mode() -> str:
    """`builtin` (Rust regex only) or a mode that adds the NER detector, read from /v1/health."""
    resp = requests.get(f"{URL}/v1/health", headers=_headers(), timeout=TIMEOUT)
    _raise_for_guard(resp)
    msg = resp.json().get("components", {}).get("detector", {}).get("message", "")
    return "builtin" if "builtin_only" in msg else "ner"


def transform_text(text: str, policy: str, language: str) -> Timed:
    return _post_json(
        "/v1/transform",
        {"input": text, "input_type": "text", "policy": policy, "language": language},
    )


def transform_json(obj: Any, policy: str, language: str) -> Timed:
    return _post_json(
        "/v1/transform",
        {"input_json": obj, "input_type": "json", "policy": policy, "language": language},
    )


def transform_file(data: bytes, filename: str, policy: str, language: str) -> Timed:
    return _post_multipart(
        "/v1/transform/file",
        {"file": (filename, data)},
        {"policy": policy, "language": language},
    )


def transform_image(data: bytes, filename: str, language: str) -> Timed:
    return _post_multipart(
        "/v1/transform/image",
        {"image": (filename, data)},
        {"language": language},
    )


def rehydrate(output: str, session_state: str, channel: str) -> Timed:
    return _post_json(
        "/v1/rehydrate",
        {"output": output, "session_state": session_state, "output_channel": channel, "restore_mode": "full"},
    )

The client is the whole API surface the demo needs: transform_text, transform_json, transform_file, transform_image, and rehydrate, each hitting a /v1/... route with a policy, a language, and (for rehydrate) an output channel. detector_mode() reads /v1/health to tell whether NER is active.

The scenario matrix

scenarios.py defines the hero flow plus the montage across industries, languages, and formats. sensitive lists every raw value that must never reach the model. ner_values is the subset that needs the NER detector. This is the complete file.

"""The demo matrix: one hero flow plus a montage across industries, languages, and input formats.

`sensitive` lists every raw value that must never reach the model. `ner_values` is the subset that
needs the NER detector (people, organisations, clinical ids); on the Rust-only engine those stay
raw, which the demo shows on purpose to make the regex-vs-NER boundary concrete. The regex engine
catches the structured identifiers (email, phone, IBAN, card, order, German tax id) in every
language, so every scenario protects real data even without NER."""

from __future__ import annotations

from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

HERE = Path(__file__).resolve().parent


def _fx(name: str) -> str:
    return (HERE / "fixtures" / name).read_text(encoding="utf-8").strip()


@dataclass
class Scenario:
    key: str
    industry: str
    language: str
    modality: str          # text | json | csv | pdf | image
    policy: str
    note: str
    sensitive: list[str]
    ner_values: list[str] = field(default_factory=list)
    hero: bool = False
    lang_api: str = "en"
    text: str = ""
    payload: Any = None
    asset: str = ""        # filename under assets/ for pdf/csv/image
    task: str = ""         # model instruction; empty = protect-only (no model call)
    reader: str = "the customer"
    reply_channel: str = "customer_email"
    preview: str = ""      # human-readable raw content for asset modalities (csv/pdf/image)


HERO = Scenario(
    key="support-en",
    industry="Customer support",
    language="English",
    modality="text",
    policy="oguardai-demo",
    note="one support ticket, protected end to end, then restored per reader",
    sensitive=[
        "Erika Mustermann",
        "erika.mustermann@example.de",
        "+49 30 55598420",
        "ORD-2026-1234",
        "4111 1111 1111 1111",
        "DE97 9900 0000 0123 4567 89",
    ],
    ner_values=["Erika Mustermann"],
    hero=True,
    lang_api="en",
    text=_fx("support_en.txt"),
    task="Write a short, friendly reply to this support ticket.",
    reader="the customer",
)

MODALITIES = [
    Scenario(
        key="chatbot-json",
        industry="Chatbot",
        language="English",
        modality="json",
        policy="oguardai-demo",
        note="a structured chatbot payload, protected field by field (safe_json)",
        sensitive=["erika.mustermann@example.de", "+49 30 12345678", "ORD-9910"],
        lang_api="en",
        payload={
            "session": "chat-7781",
            "customer": {"email": "erika.mustermann@example.de", "phone": "+49 30 12345678"},
            "message": "Where is my order ORD-9910?",
        },
    ),
    Scenario(
        key="retail-csv",
        industry="E-commerce",
        language="English",
        modality="csv",
        policy="oguardai-demo",
        note="a customer export CSV, every row protected before it leaves the boundary",
        sensitive=[
            "Max Mustermann", "max.mustermann@shop.example", "+44 20 7946 0102", "4012 8888 8888 1881",
            "Erika Mustermann", "erika.mustermann@shop.example", "+39 06 6980 1234", "5555 5555 5555 4444",
        ],
        ner_values=["Max Mustermann", "Erika Mustermann"],
        asset="retail_customers.csv",
        lang_api="en",
        preview=(
            "name,email,phone,card,country\n"
            "Max Mustermann,max.mustermann@shop.example,+44 20 7946 0102,4012 8888 8888 1881,UK\n"
            "Erika Mustermann,erika.mustermann@shop.example,+39 06 6980 1234,5555 5555 5555 4444,IT"
        ),
    ),
    Scenario(
        key="hospital-pdf",
        industry="Hospital",
        language="English",
        modality="pdf",
        policy="healthcare-clinical",
        note="a discharge summary PDF; MRN by custom regex, diagnosis and medication by zero-shot NER",
        sensitive=[
            "Max Mustermann", "max.mustermann@example.com", "+49 89 55512340",
            "MRN-0088213", "Type 2 diabetes", "Metformin",
        ],
        ner_values=["Max Mustermann", "Type 2 diabetes", "Metformin"],
        asset="hospital_discharge.pdf",
        lang_api="en",
        preview=_fx("hospital_en.txt"),
    ),
    Scenario(
        key="insurance-image",
        industry="Insurance",
        language="German",
        modality="image",
        policy="oguardai-demo",
        note="a scanned insurance card, OCR then protect (needs the OCR engine)",
        sensitive=["Erika Mustermann", "INS-7K2X-9M4P", "DE97 9900 0000 0123 4567 89"],
        ner_values=["Erika Mustermann"],
        asset="insurance_card_de.png",
        lang_api="de",
        preview=(
            "MUSTERVERSICHERUNG AG\nVersichertenkarte\n\n"
            "Name:   Erika Mustermann\nPolicy: INS-7K2X-9M4P\nIBAN:   DE97 9900 0000 0123 4567 89"
        ),
    ),
]

LANGUAGES = [
    Scenario(
        key="tax-de",
        industry="Tax office",
        language="German",
        modality="text",
        policy="oguardai-demo",
        note="German tax notice: the tax id and IBAN are caught by regex, the name by NER",
        sensitive=["Erika Mustermann", "36 574 261 809", "DE97 9900 0000 0123 4567 89", "erika.mustermann@example.de"],
        ner_values=["Erika Mustermann"],
        lang_api="de",
        text=_fx("tax_de.txt"),
        task="Draft a brief, polite reply to the taxpayer about this notice.",
        reader="the taxpayer",
    ),
    Scenario(
        key="gov-ar",
        industry="Government",
        language="Arabic",
        modality="text",
        policy="oguardai-demo",
        note="Arabic ID-renewal request: email and phone caught by regex, the name by NER",
        sensitive=["فلان الفلاني", "fulan.alfulani@example.com", "+20 2 25554321"],
        ner_values=["فلان الفلاني"],
        lang_api="ar",
        text=_fx("gov_ar.txt"),
        task="Draft a brief acknowledgement of this request, in the same language as the request.",
        reader="the applicant",
    ),
    Scenario(
        key="support-ja",
        industry="Customer support",
        language="Japanese",
        modality="text",
        policy="oguardai-demo",
        note="Japanese support ticket: order, card, email, phone caught by regex, the name by NER",
        sensitive=["山田花子", "hanako.yamada@example.jp", "+81 3 55550123", "ORD-2026-7788", "4012 8888 8888 1881"],
        ner_values=["山田花子"],
        lang_api="ja",
        text=_fx("support_ja.txt"),
        task="Write a short, friendly reply to this support ticket, in the same language as the ticket.",
        reader="the customer",
    ),
]

ALL = [HERO, *MODALITIES, *LANGUAGES]

The runner

run.py orchestrates each scenario: protect the input, enforce the boundary before the model call, draft the reply, then restore per reader. The complete run.py is shown below, then render.py, the terminal rendering it imports as r (the diagram, screens, coloured tokens, and benchmark table). generate_assets.py regenerates the checked-in PDF, CSV, and PNG under assets/.

The complete run.py:

"""OGuardAI demo runner. Every scenario protects its input before anything leaves the trusted zone, and the
model never sees a raw identifier. A scenario with a `task` goes on to call the model and restore the
answer per reader, but only if protection was complete: if a raw identifier survived, or if the value
needs the NER sidecar that is not running, the run stops before the model call and says so. A
scenario without a `task` is protect-only by design (see scenarios.py). The hero (customer support) shows all readers plus the output guard; the montage
covers formats (text, JSON, CSV, PDF, image), languages (English, German, Arabic, Japanese) and
verticals, with the real per-step latency.

    export GUARDAI_URL=http://localhost:3000
    export GUARDAI_API_KEY="<key>"        # only if the server runs with auth
    export OPENAI_API_KEY="sk-..."        # optional: draft with a real model (else an offline draft)
    python run.py                         # full demo
    python run.py --hero                  # only the hero
    python run.py --montage               # only the industry round-trips
    python run.py --show-openai           # print the exact request/response bytes for the hero
    python run.py --policy NAME           # force one policy (for a server with a limited policy set)
"""

from __future__ import annotations

import json
import os
import re
import sys
import time

import guard
import render as r
import scenarios as S

OPENAI_KEY = os.getenv("OPENAI_API_KEY", "")
OPENAI_MODEL = os.getenv("OPENAI_MODEL", "gpt-4o-mini")
PAUSE = float(os.getenv("DEMO_PAUSE", "0"))
POLICY_OVERRIDE = os.getenv("GUARDAI_POLICY", "")
SHOW_OPENAI = bool(os.getenv("SHOW_OPENAI"))
_TOKEN_RE = re.compile(r"\{\{([^}]+)\}\}")


def pause() -> None:
    if PAUSE:
        time.sleep(PAUSE)


def policy_for(sc: S.Scenario) -> str:
    return POLICY_OVERRIDE or sc.policy


def _leak(text: str, sensitive: list[str] | None) -> list[str]:
    return [v for v in (sensitive or []) if v and v in text]


def raw_display(sc: S.Scenario) -> str:
    if sc.modality == "text":
        return sc.text
    if sc.modality == "json":
        return json.dumps(sc.payload, indent=2, ensure_ascii=False)
    return sc.preview or f"[{sc.modality} asset: {sc.asset}]"


def protect(sc: S.Scenario, bench: guard.Bench) -> tuple[str, dict]:
    """Run the right transform for the scenario's modality; returns (safe_display, response)."""
    policy, lang = policy_for(sc), sc.lang_api
    if sc.modality == "text":
        t = guard.transform_text(sc.text, policy, lang)
        bench.add(sc.key, "transform", t.ms)
        return t.data.get("safe_text", ""), t.data
    if sc.modality == "json":
        t = guard.transform_json(sc.payload, policy, lang)
        bench.add(sc.key, "transform", t.ms)
        return json.dumps(t.data.get("safe_json", {}), indent=2, ensure_ascii=False), t.data
    data = (S.HERE / "assets" / sc.asset).read_bytes()
    t = guard.transform_image(data, sc.asset, lang) if sc.modality == "image" else guard.transform_file(data, sc.asset, policy, lang)
    bench.add(sc.key, "transform", t.ms)
    return t.data.get("safe_text", ""), t.data


def _split_leak(text: str, sc: S.Scenario) -> tuple[list[str], list[str]]:
    hit = [v for v in sc.sensitive if v and v in text]
    return [v for v in hit if v not in sc.ner_values], [v for v in hit if v in sc.ner_values]


class Boundary:
    """One leak record for the whole run. A structured identifier that reaches the model, or comes back
    from it, fails every lane; a person name fails only when the NER sidecar is active, otherwise it is a
    printed note. The exit decision and the closing summary both read this one record."""

    def __init__(self, ner_available: bool) -> None:
        self.ner_available = ner_available
        self.name_notes: list[str] = []

    def note_names(self, names: list[str]) -> None:
        for n in names:
            if n not in self.name_notes:
                self.name_notes.append(n)


def _ner_active() -> bool:
    try:
        data = guard.capabilities()
    except Exception:
        return True
    val = data.get("ner_active", True)
    return val is not False


def _guard_crossing(text: str, sc: S.Scenario, boundary: Boundary, where: str) -> list[str]:
    """Enforce the boundary on one string that crosses to, or returns from, the model. A structured
    identifier fails every lane; a person name fails when NER is active, else it is recorded and returned
    so the caller can print the note."""
    structured, names = _split_leak(text, sc)
    if structured:
        r.bad(f"raw identifiers reached {where}: {structured}")
        raise SystemExit(f"structured PII reached {where}: {structured}")
    if names and boundary.ner_available:
        r.bad(f"a person name reached {where}: {names}")
        raise SystemExit(f"person names reached {where}: {names}")
    if names:
        boundary.note_names(names)
    return names


def draft_reply(safe_text: str, task: str, *, show: bool = False, sensitive: list[str] | None = None) -> str:
    """One model call, handed only tokenized text. OpenAI when a key is set, else a deterministic
    offline draft that echoes tokens so every one resolves on restore. With `show`, print the exact
    request and response bytes plus a leak check, the direct proof that no raw value left."""
    tokens = ["{{" + t + "}}" for t in _TOKEN_RE.findall(safe_text)]
    if not OPENAI_KEY:
        if show:
            r.note("OPENAI_API_KEY not set: using an offline draft. Set the key to show real wire bytes.")
        keep = ", ".join(dict.fromkeys(tokens[:4]))
        return f"Thank you for your message regarding {keep}. We are on it. Kind regards, Customer Support."
    import requests

    system = (
        f"{task} The text uses semantic tokens like {{{{person:p_001:9f2a}}}}; keep every one exactly "
        "unchanged. Do not add or invent names, emails, numbers, bracketed placeholders, or a closing "
        "signature. Do not put a title, honorific, or name suffix (Mr, Ms, Mrs, Herr, Frau, -san) next "
        "to a token; the system restores the correct form."
    )
    body = {
        "model": OPENAI_MODEL,
        "temperature": 0.3,
        "messages": [{"role": "system", "content": system}, {"role": "user", "content": safe_text}],
    }
    if show:
        r.step("OPENAI REQUEST", "exact payload sent to api.openai.com/v1/chat/completions")
        r.note(f"model {OPENAI_MODEL}    temperature 0.3")
        for msg in body["messages"]:
            r.note(f'{msg["role"]}:')
            r.body(msg["content"], kind="safe")
        (r.ok if not _leak(json.dumps(body, ensure_ascii=False), sensitive) else r.bad)("leak check passed: 0 raw identifiers in request")
    resp = requests.post(
        "https://api.openai.com/v1/chat/completions",
        headers={"Authorization": f"Bearer {OPENAI_KEY}", "Content-Type": "application/json"},
        json=body,
        timeout=60,
    )
    resp.raise_for_status()
    data = resp.json()
    content = data["choices"][0]["message"]["content"].strip()
    if show:
        u = data.get("usage", {})
        r.step("OPENAI RESPONSE", f"{data.get('model')} · {u.get('total_tokens')} tokens")
        r.body(content, kind="safe")
        (r.ok if not _leak(json.dumps(data, ensure_ascii=False), sensitive) else r.bad)("leak check passed: 0 raw identifiers in response")
    return content


def run_scenario(sc: S.Scenario, mode: str, bench: guard.Bench, boundary: Boundary, detailed: bool = False) -> None:
    r.card(sc.industry.upper(), f"{sc.language} · {sc.modality} · policy {policy_for(sc)}")
    r.note(sc.note)

    r.step("RAW INPUT", "real data, before OGuardAI")
    r.body(raw_display(sc), kind="raw", raw=sc.sensitive)
    pause()

    try:
        safe, resp = protect(sc, bench)
    except guard.GuardError as exc:
        if sc.modality == "image":
            r.note(f"OCR needs Tesseract on the server (full stack only): {exc}")
        else:
            r.note(f"unavailable here (the full stack has every policy): {exc}")
        return

    if sc.task:
        r.step("PROTECTED PROMPT", "this is exactly what the model receives")
    else:
        r.step("PROTECTED", "sensitive values tokenized before the boundary")
    r.body(safe, kind="safe")
    names = _guard_crossing(safe, sc, boundary, "the model prompt")
    if names:
        r.ok(f"regex protected {len(sc.sensitive) - len(names)} of {len(sc.sensitive)} identifiers")
        r.note(f"NER adds {names} on the full stack; not calling the model, so nothing raw is sent")
        return
    r.ok(f"0 of {len(sc.sensitive)} raw identifiers reached the model")
    pause()

    if not sc.task:
        return

    # With --show-openai the WIRE screens already print the response, so do not print it twice.
    # Without a key there are no wire screens, so the model screen must still be shown.
    wire = detailed and SHOW_OPENAI and bool(OPENAI_KEY)
    if not wire:
        r.step("MODEL", ("gpt-4o-mini" if OPENAI_KEY else "offline draft") + " · sees only tokens")
    t0 = time.perf_counter()
    model_output = draft_reply(safe, sc.task, show=wire, sensitive=sc.sensitive)
    bench.add(sc.key, "model", (time.perf_counter() - t0) * 1000.0)
    if not wire:
        r.body(model_output, kind="safe")
    returned = _guard_crossing(model_output, sc, boundary, "the model response")
    if returned:
        r.note(f"the model response returned {returned}; this runtime runs without NER, so the name was not tokenized")
    pause()

    session = resp["session_state"]
    r.step("RESTORED OUTPUT", f"for {sc.reader} · channel {sc.reply_channel}")
    out = guard.rehydrate(model_output, session, sc.reply_channel)
    bench.add(sc.key, "restore", out.ms)
    r.body(out.data["restored_text"], kind="real", raw=sc.sensitive)

    if not detailed:
        return

    r.step("RESTORE BY CHANNEL", "one model response, restored differently by policy")
    out = guard.rehydrate(model_output, session, "export")
    bench.add(sc.key, "restore:internal", out.ms)
    r.note("internal system, masked:")
    r.body(out.data["restored_text"], kind="safe")
    out = guard.rehydrate(model_output, session, "log_safe")
    bench.add(sc.key, "restore:audit", out.ms)
    r.note("audit log, removed:")
    r.body(out.data["restored_text"], kind="safe")

    r.step("OUTPUT GUARD", "new PII generated by the model is detected before delivery")
    unsafe = "Internal note: for anything urgent reach the on-call line at ops@internal.example or +49 89 99001122."
    injected = ["ops@internal.example", "+49 89 99001122"]
    r.note("simulated unsafe model addition (injected for the demo, not from the request):")
    r.body(unsafe, kind="raw", raw=injected)
    try:
        out = guard.rehydrate(model_output + "\n" + unsafe, session, sc.reply_channel)
        lines = out.data["restored_text"].splitlines()
        guarded = next((ln for ln in lines if "Internal note" in ln), lines[-1] if lines else "")
        caught = not any(v in guarded for v in injected)
        r.note("guarded output:")
        r.body(guarded, kind="safe")
        (r.ok if caught else r.bad)("newly generated PII masked" if caught else "newly generated PII survived")
        if not caught:
            raise SystemExit("output guard failed: newly generated PII survived into the delivered output")
    except guard.GuardError:
        r.ok("response blocked: newly generated PII detected")


def main(argv: list[str]) -> int:
    only_hero = "--hero" in argv
    only_montage = "--montage" in argv
    if "--policy" in argv:
        globals()["POLICY_OVERRIDE"] = argv[argv.index("--policy") + 1]
    if "--show-openai" in argv:
        globals()["SHOW_OPENAI"] = True

    try:
        mode = guard.detector_mode()
    except Exception as exc:
        print(f"cannot reach OGuardAI at {guard.URL}: {exc}")
        print("start the runtime first (see README), then re-run.")
        print("if it is already running on another port, point the demo at it:")
        print("  GUARDAI_URL=http://localhost:<port> python run.py")
        return 1

    boundary = Boundary(_ner_active())

    r.banner("OGuardAI", "data-control layer for production AI")
    r.diagram()
    if "--show-policy" in argv:
        r.policy_summary(str(S.HERE / "policies" / S.HERO.policy / "policy.yaml"), mode)
    r.mode_banner(mode)

    bench = guard.Bench()
    if not only_montage:
        run_scenario(S.HERO, mode, bench, boundary, detailed=True)
    if not only_hero:
        if not only_montage:
            r.transition(
                "BEYOND TEXT",
                "Same runtime. Same policy engine.",
                "Structured data · Documents · OCR · Multilingual",
            )
        for sc in [*S.MODALITIES, *S.LANGUAGES]:
            run_scenario(sc, mode, bench, boundary)

    r.bench_table(bench.rows)
    protection = (
        "Protected identifiers stay inside the trusted boundary."
        if not boundary.name_notes
        else "Structured identifiers stay inside the trusted boundary. "
        f"Names ({', '.join(boundary.name_notes)}) need the NER detector, which this runtime is not running."
    )
    r.footer(
        [
            "One model response. Policy-controlled output for every reader.",
            protection,
            "Text · JSON · CSV · PDF · OCR · multilingual",
        ]
    )
    return 0


if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))

The runner imports render.py as r for all terminal output: the flow diagram, per-scenario cards, labelled steps, coloured tokens and restored values, and the closing benchmark table. This is the complete file.

"""Terminal rendering for the demo, tuned for a screen recording: a flow diagram, per-scenario cards,
labelled steps, coloured tokens / raw values / restored values, and a benchmark table. The output
explains itself without narration. Colour is on for a TTY and off under NO_COLOR."""

from __future__ import annotations

import os
import re
import statistics
import sys
import textwrap
import unicodedata

W = 78
IND = "      "
WRAP = 92  # wrap content to this width (plus the indent) so nothing clips off a recorded terminal
_TOKEN_RE = re.compile(r"\{\{[^}]+\}\}")
_REDACT_RE = re.compile(r"\[(?:REDACTED|PERSON|EMAIL|PHONE|COMPANY|LOCATION|ADDRESS)(?::[a-z_]+)?\]")
_COLOR = sys.stdout.isatty() and os.getenv("NO_COLOR") is None

_C = {
    "reset": "\033[0m",
    "dim": "\033[2m",
    "bold": "\033[1m",
    "title": "\033[1;97m",
    "accent": "\033[38;5;39m",   # blue: structure
    "token": "\033[38;5;44m",    # cyan: safe semantic tokens
    "raw": "\033[38;5;203m",     # red: real sensitive values
    "real": "\033[38;5;114m",    # green: legitimately restored values
    "redact": "\033[38;5;170m",  # magenta: removed
    "label": "\033[1;38;5;45m",  # bold bright cyan: step headers
}


def c(key: str, text: str) -> str:
    return f"{_C[key]}{text}{_C['reset']}" if _COLOR else text


def _rule(char: str = "─") -> str:
    return c("dim", char * W)


def banner(title: str, subtitle: str) -> None:
    print()
    print("  " + c("title", title) + c("dim", "   " + subtitle))
    print(_rule("━"))


def diagram() -> None:
    lines = [
        "  raw request            OGuardAI runtime            model provider",
        "  name, email, card      the trusted data boundary    OpenAI / Claude / local",
        "",
        "  RAW  ─▶  DETECT + TOKENIZE  ─▶  SAFE PROMPT  ─▶  MODEL",
        "                                                        │",
        "  OUTPUT  ◀─  REHYDRATE per policy + channel  ◀─  RESPONSE",
        "",
        "  restore per reader:   customer ▸ real     internal ▸ masked     audit ▸ removed",
    ]
    for ln in lines:
        print(c("dim", ln))
    print(_rule())


def mode_banner(mode: str) -> None:
    dot = c("real", "●") if mode != "builtin" else c("accent", "●")
    engine = "Rust regex + NER detector" if mode != "builtin" else "Rust regex detectors only"
    print("  " + dot + " engine: " + c("bold", engine))
    if mode == "builtin":
        print(c("dim", "    structured identifiers in any language; names and orgs need the NER detector"))
    else:
        print(c("dim", "    structured identifiers by regex, people and organisations by NER, any language"))
    print(_rule())


def card(title: str, meta: str) -> None:
    print()
    print(_rule("━"))
    pad = max(2, W - len(title) - len(meta) - 2)
    print("  " + c("title", title) + " " * pad + c("dim", meta))
    print(_rule("━"))


def transition(title: str, *lines: str) -> None:
    # A section break between the hero flow and the montage, so the narration has a beat to say
    # "everything so far was one text prompt; the same runtime also handles the rest".
    print()
    print(_rule("━"))
    print("  " + c("title", title))
    for text in lines:
        for ln in _wrap_line(text, WRAP):
            print(IND + c("dim", ln))
    print(_rule("━"))


def step(label: str, note: str = "") -> None:
    print()
    print("  " + c("label", label.upper()) + (c("dim", "    " + note) if note else ""))
    print(_rule())


def policy_summary(path: str, mode: str = "ner") -> None:
    """A short, glanceable view of a policy: each entity's action and restore mode, the exposed
    metadata, the detection pipeline, and which entities fail closed. Enough to show the product is
    rule-driven without reading a full file on screen."""
    import pathlib

    p = pathlib.Path(path)
    if not p.exists():
        return
    text = p.read_text(encoding="utf-8")
    name_m = re.search(r'name:\s*"?([\w-]+)', text)
    name = name_m.group(1) if name_m else p.parent.name

    rules: list[tuple[str, str, str]] = []
    entity = action = restore = None
    in_rules = False
    for line in text.splitlines():
        if re.match(r"\s*rules:\s*$", line):
            in_rules = True
            continue
        if in_rules and line and not line[0].isspace():
            break
        if not in_rules:
            continue
        m = re.search(r'entity_type:\s*"?([\w:]+)', line)
        if m:
            if entity:
                rules.append((entity, action or "tokenize", restore or ""))
            entity, action, restore = m.group(1), None, None
            continue
        a = re.search(r'\baction:\s*"?(\w+)', line)
        if a and action is None:
            action = a.group(1)
        rm = re.search(r'\brestore_mode:\s*"?(\w+)', line)
        if rm and restore is None:
            restore = rm.group(1)
    if entity:
        rules.append((entity, action or "tokenize", restore or ""))

    custom = set(re.findall(r'entity_type:\s*"?(\w+)', text.split("custom_patterns:", 1)[1])) if "custom_patterns:" in text else set()
    meta = re.findall(r"expose_(\w+):\s*true", text)

    fail_closed: list[str] = []
    in_req = False
    for line in text.splitlines():
        if re.match(r"\s*required_for:\s*$", line):
            in_req = True
            continue
        if in_req:
            m = re.match(r'\s*-\s*"?(\w+)', line)
            if m:
                fail_closed.append(m.group(1))
            elif line.strip():
                break

    card("POLICY", f"how every entity is protected and restored · {name}")
    print()
    for ent, act, res in rules:
        col = "redact" if act in ("redact", "block") else "token"
        rest = c("dim", f"restore {res}") if res else ""
        tag = c("dim", "  (custom regex)") if ent in custom else ""
        print(IND + f"{ent:<18}" + c(col, f"{act:<10}") + rest + tag)
    print()
    print(IND + f"{'detection':<18}" + c("token", "rust regex + NER" if mode != "builtin" else "rust regex"))
    if fail_closed:
        print(IND + f"{'fail_closed':<18}" + c("redact", ", ".join(fail_closed)))
    if meta:
        print(IND + f"{'safe context':<18}" + c("dim", ", ".join(meta) + " (never the raw value)"))


def _paint(text: str) -> str:
    text = _TOKEN_RE.sub(lambda m: c("token", m.group(0)), text)
    return _REDACT_RE.sub(lambda m: c("redact", m.group(0)), text)


_WRAP_ATOM_RE = re.compile(r"\{\{[^}]*\}\}|\[[^\]]*\]")


def _dwidth(s: str) -> int:
    # CJK and other East-Asian wide/fullwidth glyphs occupy two terminal columns; textwrap counts them
    # as one, so a Japanese line wrapped by character count can still run twice as wide on screen.
    return sum(2 if unicodedata.east_asian_width(ch) in ("W", "F") else 1 for ch in s)


def _has_wide(s: str) -> bool:
    return any(unicodedata.east_asian_width(ch) in ("W", "F") for ch in s)


def _atoms(src: str) -> list[str]:
    # Break a line into units that may each start a new wrapped line but are never split internally:
    # whole tokens / redactions, whitespace runs, Latin words, and single wide (CJK) characters, since
    # CJK text has no spaces to break on.
    def atomize(text: str) -> list[str]:
        out: list[str] = []
        for part in re.split(r"(\s+)", text):
            if not part:
                continue
            if part.isspace() or not _has_wide(part):
                out.append(part)
                continue
            buf = ""
            for ch in part:
                if unicodedata.east_asian_width(ch) in ("W", "F"):
                    if buf:
                        out.append(buf)
                        buf = ""
                    out.append(ch)
                else:
                    buf += ch
            if buf:
                out.append(buf)
        return out

    atoms: list[str] = []
    i = 0
    for m in _WRAP_ATOM_RE.finditer(src):
        atoms.extend(atomize(src[i : m.start()]))
        atoms.append(m.group(0))
        i = m.end()
    atoms.extend(atomize(src[i:]))
    return atoms


def _wrap_display(src: str, width: int) -> list[str]:
    lines: list[str] = []
    cur, curw = "", 0
    for a in _atoms(src):
        aw = _dwidth(a)
        if a.isspace():
            if cur and curw + aw <= width:
                cur += a
                curw += aw
            elif cur:
                lines.append(cur.rstrip())
                cur, curw = "", 0
            continue
        if cur and curw + aw > width:
            lines.append(cur.rstrip())
            cur, curw = "", 0
        cur += a
        curw += aw
    if cur.strip():
        lines.append(cur.rstrip())
    return lines or [""]


def _wrap_line(src: str, width: int) -> list[str]:
    # Latin/Arabic (single-width) lines keep the proven textwrap path; only wide-glyph lines use the
    # display-width wrapper, so nothing overflows a recorded terminal.
    if not _has_wide(src):
        return textwrap.wrap(src, width=width, break_long_words=False, break_on_hyphens=False) or [""]
    return _wrap_display(src, width)


def body(text: str, *, kind: str = "plain", raw: list[str] | None = None) -> None:
    # Wrap each source line to a fixed width first (tokens have no spaces, so they never split), then
    # colour each display line, so nothing runs off the edge of a recorded terminal.
    wrapped: list[str] = []
    for src in text.splitlines() or [""]:
        if not src.strip():
            wrapped.append("")
            continue
        wrapped.extend(_wrap_line(src, WRAP))
    for line in wrapped:
        if kind == "raw" and raw:
            for v in sorted(raw, key=len, reverse=True):
                if v and v in line:
                    line = line.replace(v, c("raw", v))
        elif kind == "safe":
            line = _paint(line)
        elif kind == "real" and raw is not None:
            line = _paint(line)
            for v in sorted(raw, key=len, reverse=True):
                if v and v in line:
                    line = line.replace(v, c("real", v))
        print(IND + line)


def ok(msg: str) -> None:
    print(IND + c("real", "✓ ") + c("dim", msg))


def bad(msg: str) -> None:
    print(IND + c("raw", "✗ ") + msg)


def note(text: str) -> None:
    print("  " + c("dim", text))


def bench_table(rows: list[tuple[str, str, float]]) -> None:
    if not rows:
        return
    card("LIVE RUN LATENCY", "wall-clock measurements from this execution")
    name_w = max(len(r[0]) for r in rows) + 2
    step_w = max(len(r[1]) for r in rows) + 2
    peak = max(r[2] for r in rows) or 1.0
    print()
    for scenario, stp, ms in rows:
        bar = c("accent", "▇" * max(1, round(ms / peak * 34)))
        print(f"  {scenario:<{name_w}}{c('dim', stp):<{step_w + 9}}{ms:7.1f} ms  {bar}")
    print(_rule())
    transform = [ms for _, s, ms in rows if s == "transform"]
    restore = [ms for _, s, ms in rows if s.startswith("restore")]
    model = [ms for _, s, ms in rows if s == "model"]
    parts = []
    if transform:
        parts.append(f"median OGuardAI transform {statistics.median(transform):.0f} ms")
    if restore:
        parts.append(f"median restore {statistics.median(restore):.0f} ms")
    if model:
        parts.append(f"model calls {min(model) / 1000:.1f}-{max(model) / 1000:.1f} s")
    print(c("dim", "  " + " · ".join(parts)))
    if model:
        print("  " + c("dim", "The runtime adds milliseconds. The external model call remains the dominant cost."))


def footer(lines: list[str]) -> None:
    print()
    print(_rule("━"))
    for ln in lines:
        print("  " + c("dim", ln))
    print(_rule("━"))
    print()

Configuration and policy

The demo server runs on oguardai-demo.yaml. NER on for ingress, dev auth for a frictionless local run, and a regex output guard. This is the complete file.

# Server config for the demo. NER detector on (people, organisations, multilingual names), dev auth
# for a frictionless local run, and a regex output guard.
#
# The output guard exempts policy-authorized restores (a customer_email reply keeps the real name and
# order) and masks PII the model INVENTS that was never in the session. NER output scanning is left off:
# on multilingual model prose it over-detects common words (a German "E-Mail-Adresse", a Japanese
# honorific) as entities and masks them, which reads as a false positive on screen. Ingress still runs
# full NER (detector.mode both), so names are tokenized before the model call regardless.

server:
  host: 0.0.0.0
  port: 3000

auth:
  mode: dev

session:
  backend: sealed
  secret: ${GUARDAI_SESSION_SECRET:-guardai-dev-secret-change-in-production}
  ttl_seconds: 3600

detector:
  mode: both
  advanced_url: http://localhost:9090
  scan_output_with_ner: false

policy:
  default: oguardai-demo
  # Staged by the README's step 2; never the shipped ./policies, whose set is pinned by a test.
  directory: ./target/demo-policies

transform:
  context_strategy: full
  max_context_tokens: 4096

output_protection:
  enabled: true
  mode: strict
  default_action: mask
  exempt_types:
    - url

prompt_security:
  enabled: true
  input_scanning: true
  action: strip

rate_limit:
  enabled: false

The demo policy makes the per-reader story concrete: the customer sees real values, internal systems see masked, the audit log sees nothing. It ships in policies/oguardai-demo/policy.yaml inside the example folder. This is the complete file.

name: "oguardai-demo"
version: "1.0.0"
description: "Demo policy: the customer sees real values, internal systems see masked, the audit log sees nothing."

# Per-entity rules. Each type gets an action (tokenize, redact, abstract) and a restore mode
# (full, formatted, partial, masked, abstract, none). Anything unlisted falls through to defaults.
rules:
  - entity_type: "person"
    protection_level: 2
    action: "tokenize"
    restore_mode: "full"               # customer channel restores the real name; the safe channels
                                       # (export masks, log_safe removes) clamp it down regardless
  - entity_type: "email"
    protection_level: 2
    action: "tokenize"
    restore_mode: "full"
  - entity_type: "phone"
    protection_level: 2
    action: "tokenize"
    restore_mode: "full"
  - entity_type: "order"
    protection_level: 2
    action: "tokenize"
    restore_mode: "full"
  - entity_type: "iban"
    protection_level: 1
    action: "tokenize"
    restore_mode: "masked"             # account number never restored in full
  - entity_type: "credit_card"
    protection_level: 1
    action: "redact"                   # removed outright, no channel brings it back
  - entity_type: "ssn"
    protection_level: 1
    action: "redact"
  - entity_type: "passport"
    protection_level: 1
    action: "redact"
  - entity_type: "case_ref"
    protection_level: 2
    action: "tokenize"
    restore_mode: "full"
  - entity_type: "insurance_policy"
    protection_level: 2
    action: "tokenize"
    restore_mode: "full"

# Custom regex: define your own identifiers without touching the runtime.
custom_patterns:
  - entity_type: "case_ref"
    pattern: '\bCASE-\d{4}-\d{4}\b'
    confidence: 0.95
    context_words: ["case", "ticket", "ref"]
  - entity_type: "insurance_policy"
    pattern: '\bINS-[A-Z0-9]{4}-[A-Z0-9]{4}\b'
    confidence: 0.95
    context_words: ["policy", "versicherung", "insurance"]

# Anything not listed above.
defaults:
  protection_level: 2
  action: "tokenize"
  restore_mode: "full"

# Safe context the model may see (never the raw value), so a reply is correctly gendered.
metadata_policy:
  expose_gender: true
  expose_formality: true
  expose_language: true
  expose_role: true

requirements.txt keeps the demo runtime to just the HTTP client:

# Demo runtime: only the HTTP client is required.
requests>=2.31

# Optional, only to regenerate the binary assets under assets/ (already checked in):
#   fpdf2>=2.7
#   pillow>=10.0

Fixtures

Each scenario reads a raw input from fixtures/. All values are fictional. The assets/ folder holds the pre-generated PDF (hospital_discharge.pdf), CSV (retail_customers.csv), and PNG (insurance_card_de.png) that the file and image scenarios use.

fixtures/support_en.txt (the hero ticket):

Subject: Charged twice for my order

Hello, this is Erika Mustermann. I am writing about order ORD-2026-1234.
My card 4111 1111 1111 1111 was charged twice for the same item.
You can reach me at erika.mustermann@example.de or on +49 30 55598420.
My account IBAN is DE97 9900 0000 0123 4567 89. Please refund the duplicate charge.

fixtures/tax_de.txt (German tax notice):

Finanzamt Muenchen - Steuerbescheid 2025

Sehr geehrte Frau Erika Mustermann,
Ihre Steuer-Identifikationsnummer lautet 36 574 261 809.
Die Erstattung wird auf Ihr Konto DE97 9900 0000 0123 4567 89 ueberwiesen.
Rueckfragen bitte an erika.mustermann@example.de. Mit freundlichen Gruessen, Ihr Finanzamt.

fixtures/gov_ar.txt (Arabic ID-renewal request):

وزارة الداخلية - طلب تجديد بطاقة الهوية

مقدم الطلب: فلان الفلاني
البريد الإلكتروني: fulan.alfulani@example.com
رقم الهاتف: +20 2 25554321
يرجى إرسال بطاقة الهوية الجديدة إلى العنوان المسجل في الملف.

fixtures/support_ja.txt (Japanese support ticket):

件名: 注文が二重に請求されました

お世話になっております。山田花子と申します。注文番号 ORD-2026-7788 について連絡しています。
クレジットカード 4012 8888 8888 1881 が二重に請求されました。
連絡先は hanako.yamada@example.jp または +81 3 55550123 です。返金をお願いします。

fixtures/hospital_en.txt (discharge summary preview; the PDF asset carries the same content):

MUSTER GENERAL HOSPITAL - DISCHARGE SUMMARY

Patient: Max Mustermann      Record type: Inpatient
Contact: max.mustermann@example.com      Phone: +49 89 55512340
Record ID: MRN-0088213

Diagnosis: Type 2 diabetes, follow-up in 6 weeks. Metformin 500mg twice daily.
Please send the lab results to the patient email above.

Expected output

The demo prints a title, the boundary diagram, and a mode banner, then walks each scenario as a sequence of labeled screens. The hero flow (customer support) prints these steps:

  1. RAW INPUT (real data, before OGuardAI): the raw ticket with the sensitive values highlighted.
  2. PROTECTED PROMPT (this is exactly what the model receives): the same text with every value replaced by a semantic token. On the full stack the name is tokenized too; on the Rust-only preview the demo reports the name it could not tokenize and stops before the model call, so nothing raw is sent. The confirmation line reads 0 of 6 raw identifiers reached the model.
  3. MODEL (gpt-4o-mini · sees only tokens, or offline draft · sees only tokens without a key): the drafted reply, still in tokens. The offline draft is deterministic: Thank you for your message regarding <tokens>. We are on it. Kind regards, Customer Support.
  4. RESTORED OUTPUT (for the customer · channel customer_email): the reply with the customer's real values restored.
  5. RESTORE BY CHANNEL (one model response, restored differently by policy): the same response restored again for the export channel (internal system, masked) and the log_safe channel (audit log, removed).
  6. OUTPUT GUARD (new PII generated by the model is detected before delivery): an email and phone number that were never in the request are injected into the model output; the guard masks them and the line reads newly generated PII masked.

The montage then runs the format, industry, and language scenarios. A scenario with a task (tax office, government, Japanese support) makes the full round trip to the model and back in its own language. A scenario without a task (chatbot JSON, e-commerce CSV, hospital PDF, insurance image) is protect-only and exercises the JSON, CSV, PDF, and OCR paths. On the full stack the hospital PDF tokenizes the MRN by custom regex and the diagnosis and medication by zero-shot NER.

The run ends with a benchmark table of the real latency the runtime added per call. On a warm builtin server (Rust regex only) the README documents these measured figures:

StepBuiltin (Rust regex)With GLiNER NER
Text transformabout 5 ms180 to 400 ms
JSON transformabout 2 msaround 1 s (field by field)
CSV transformabout 2 ms(regex figures apply)
PDF extractionabout 23 ms(regex figures apply)
OCR imageabout 200 msabout 500 ms
Restore3 to 5 ms3 to 5 ms

These are measured, not estimated, and the NER numbers move with the sidecar's hardware.

Flags and environment

Command-line flags for run.py:

FlagWhat it does
--herorun only the hero flow (customer support)
--montagerun only the industry, format and language scenarios (some protect-only)
--policy NAMEforce one policy for every scenario (for a server with a limited policy set)
--show-openaiprint the exact request and response bytes to OpenAI, each with a leak check (needs OPENAI_API_KEY)
--show-policyprint the governing policy YAML right after the diagram

Environment variables the demo reads:

VariableDefaultWhat it does
GUARDAI_URLhttp://localhost:3000the OGuardAI runtime URL
GUARDAI_API_KEY(none)API key, only if the server runs with auth
OPENAI_API_KEY(none)draft with a real model; without it an offline draft is used
OPENAI_MODELgpt-4o-minithe model name
DEMO_PAUSE0seconds to pause between screens, for recording
GUARDAI_POLICY(per scenario)force one policy, same as --policy
SHOW_OPENAI(unset)same as --show-openai
NO_COLOR(unset)disable colour
GUARDAI_TIMEOUT60HTTP timeout in seconds

Files

FileWhat it is
run.pythe runner: diagram, hero flow, montage, benchmark
guard.pytimed OGuardAI HTTP client (transform, rehydrate, file, image)
render.pyterminal rendering: diagram, screens, coloured tokens, benchmark table
scenarios.pythe scenario matrix (industry, language, format, policy, fixtures)
generate_assets.pyregenerates the PDF, CSV, and PNG under assets/
fixtures/the raw inputs, one per scenario, in their native languages
assets/pre-generated PDF, CSV, and PNG, checked in
policies/oguardai-demo/the demo policy: customer sees real values, internal masked, log removed

Next steps

  • Quick Start: the minimal transform and rehydrate calls
  • Installation: every deployment path, including Docker Compose with NER
  • Configuration: the full config file and environment reference