Skip to content

Privacy & data handling

Undercurrent runs inside your inference process and sees the same data your model does. This page lists what each component touches, what leaves the process and how to control it. As the operator, you decide what gets recorded, where it goes and how long it is kept.

No telemetry

Undercurrent does not phone home. It collects no usage statistics, crash reports or analytics, and it makes no network calls of its own except to endpoints you configure.

We checked this by searching the package source (src/undercurrent/) for network clients (urllib, requests, httpx, aiohttp, http.client, socket, gRPC, huggingface_hub) and for telemetry or analytics libraries. There are exactly two ways the package reaches the network:

What When Where it goes
WebhookLogSink Only if you create one The URL you pass it (an HTTP POST with a JSON body)
Model and tokenizer downloads When you load a model by Hub ID The Hugging Face Hub, through Transformers' from_pretrained (HF adapter) or through vLLM's own model loader (vLLM adapter)

Model downloads are made by Transformers and vLLM, not by Undercurrent, and they follow those libraries' settings. To stay fully offline, pass a local model path, or set HF_HUB_OFFLINE=1 once the model is cached. vLLM has its own optional usage-statistics reporting, which is separate from Undercurrent. See the vLLM documentation for how to turn it off (for example VLLM_NO_USAGE_STATS=1).

The OpenAI-compatible server example listens for HTTP requests, but it is example code and isn't installed with the package.

What each component touches

Component Data it handles Where it lives
Adapters (undercurrent.adapters.hf, .vllm) Activations at the layers and positions your spec selects. The prompt string, model name and prompt length, passed to probes as RequestContext.prompt_metadata ({"model", "prompt", "prompt_len"}). In memory. Captured activations are copied to the CPU and handed to the router. With vLLM, when the engine runs in a separate process, activations and abort decisions cross between processes through vLLM's own RPC channel.
Router (undercurrent.router) ActivationRecords (one activation tensor plus request ID, extraction-point name, layer and token position), and probe signals and results In memory, for the lifetime of the request
Probes (your code) Anything above. A probe can read the raw prompt from prompt_metadata. Whatever your probe does with it
Metrics (Router.get_metrics, MetricsSink) Counts and timings per (request ID, extraction point): queue depth, drops, activation latency and probe errors. No activations or text. In memory, and in any MetricsSink you attach
Observation sinks (undercurrent.sinks) Probe signals and results, serialised as JSON records (see below) A local file, or a webhook you configure
Python logging Warnings about probe errors, dropped records and circuit-breaker trips. These include request IDs and extraction-point names, and the exceptions (messages and tracebacks) your probes raise. Wherever you route Python logging

Undercurrent never writes activation tensors to a sink. Tensors and raw bytes in probe output are reduced to a summary (type, shape, dtype, device or length) before they are logged. The library itself never writes prompt or generated text into a sink record. That only happens if a probe copies it into its verdict, metadata or exception messages.

What a sink record contains

Both built-in sinks write the same record shape:

{
  "kind": "result",
  "request_id": "req-42",
  "extraction_point_name": "harm_score_l16",
  "timestamp": 1790000000.0,
  "payload": {
    "request_id": "req-42",
    "extraction_point_name": "harm_score_l16",
    "verdict": "...probe-defined...",
    "signal_history": ["...signal payloads..."],
    "metadata": {"...": "probe-defined"}
  }
}

For a "signal" record, payload holds action, confidence, timestamp and a probe-defined metadata dict.

  • kind and timestamp are generated by the library.
  • extraction_point_name comes from your spec.
  • request_id is whatever your serving code or the adapter uses. It is normally opaque, but don't put user identifiers in it.
  • verdict and metadata are defined by the probe and may contain anything the probe puts there. This is where personal data usually ends up.

What leaves the process, by sink

Sink Destination Default redaction
None attached Nothing is recorded by Undercurrent. Results are only returned to your own code. —
FileLogSink(path) NDJSON lines appended to a local file None. The data stays on the machine, so records are written as they are.
WebhookLogSink(url) JSON POST to url, from a background thread, with retries Prompt text is redacted. Any key named in DEFAULT_PROMPT_TEXT_KEYS (prompt, text, generated_text, completion, messages, input_ids, token_ids and others), at any depth, has its value replaced with "[REDACTED]".
WebhookLogSink(..., dead_letter_path=...) Records that still fail after the retries go to a local NDJSON file Records are redacted before the first send attempt, so the dead-letter file and error logs only see the redacted version.

WebhookLogSink sends no authentication headers of its own and accepts any URL scheme. Use an HTTPS endpoint you control. See SECURITY.md.

Redacting and allowlisting fields

Every built-in sink takes a redact function: (record: dict) -> dict | None. It runs on a copy of each record just before the record is written or sent. Returning None drops the record. If the function raises an exception, the record is dropped rather than sent unredacted, and the sink's redaction_error_count goes up.

Redact or drop keys with the helpers. A plain key matches at any depth. A dotted path such as metadata.raw_scores matches that chain of keys.

from undercurrent.sinks import FileLogSink, WebhookLogSink, chain, drop_keys, redact_keys

# Local file: redact a user ID your probes record, drop a bulky debug field.
file_sink = FileLogSink(
    "observations.ndjson",
    redact=chain(redact_keys("user_id"), drop_keys("metadata.raw_scores")),
)

# Webhook: the default prompt-text redaction runs first, then yours.
webhook = WebhookLogSink(
    "https://observability.example.com/hook",
    dead_letter_path="dead_letters.ndjson",
    redact=redact_keys("error", "user_id"),
)

The router may log a probe's exception message under metadata["error"]. Redact error if your probes might include input text in their exceptions.

To keep data to a minimum, use an allowlist instead: send only the fields you have decided to share, and drop everything else.

from undercurrent.sinks import WebhookLogSink

ALLOWED_PAYLOAD_KEYS = {"action", "confidence", "verdict"}


def allowlist(record):
    if record["kind"] != "result":
        return None  # only send final results, never intermediate signals
    return {
        "kind": record["kind"],
        "request_id": record["request_id"],
        "extraction_point_name": record["extraction_point_name"],
        "timestamp": record["timestamp"],
        "payload": {k: v for k, v in record["payload"].items() if k in ALLOWED_PAYLOAD_KEYS},
    }


webhook = WebhookLogSink("https://observability.example.com/hook", redact=allowlist)

verdict is defined by the probe. Only allowlist it if you know your probes put a label or score there, not text.

Sending prompt text to a webhook is opt-in: WebhookLogSink(url, include_prompt_text=True). Only do this if the receiving system is cleared to hold that data.

Retention

Undercurrent doesn't rotate, expire or delete anything it writes. Retention of sink files, dead-letter files, webhook destinations, metrics backends and application logs is the operator's responsibility. Set a retention period that fits why you collect the data, enforce it with your log rotation or storage policy, and restrict access to the files (they may hold data derived from user prompts).

GDPR and PII checklist for operators

Not legal advice

This checklist is general guidance to help you think through data protection when you deploy Undercurrent. It is not legal advice and doesn't guarantee compliance with the GDPR or any other law. Ask your own legal counsel or data protection officer about your deployment.

  • [ ] Map the data. List which probes run, what each one puts in its verdict and metadata, and which sinks and metrics backends receive it.
  • [ ] Identify personal data. Prompts, generated text, request IDs tied to user accounts and probe verdicts about a person's content can all be personal data. Some probes may produce special-category data, such as inferences about health or beliefs.
  • [ ] Have a lawful basis and a stated purpose for monitoring and for each kind of record you keep. Don't reuse the data for unrelated purposes.
  • [ ] Minimise. Prefer allowlists to blocklists. Log scores and labels, not text. Attach sinks only where you need the records.
  • [ ] Be transparent. Tell users in your privacy notice that model activity is monitored and what is logged.
  • [ ] Assess automated decisions. If an inline probe blocks or aborts responses in a way that significantly affects people, check whether rules on automated decision-making apply, and offer a way to contest it.
  • [ ] Run a DPIA where required, for example for large-scale monitoring or sensitive inferences.
  • [ ] Secure transfers and storage. Use HTTPS for webhooks, restrict access to sink and dead-letter files, and check where the webhook receiver stores the data, including international transfers.
  • [ ] Set retention periods and delete data on schedule (see Retention).
  • [ ] Support data-subject rights. Make sure you can find and delete or export one person's records if asked, for example by request ID.
  • [ ] Sign processor agreements with any third party that receives sink data, such as a hosted logging or observability service.

See also Intended use & limitations and Observation sinks.