Skip to content

Privacy

The privacy subsystem has three layers:

  1. Default-deny defaults. Sensitive by default; free models forbidden for sensitive content.
  2. Content-level scanning. The privacy gate scans message content for likely secrets and silently upgrades sensitive=false to sensitive=true if it finds any.
  3. Redaction. Detected secrets are replaced with bracketed placeholders before the upstream call.

What is sensitive?

sensitive=true is the default. The policy applies the same way to every request that does not explicitly opt out.

sensitive=false is an opt-in. It is appropriate when:

  • the work is on public open-source code that you own,
  • the work is on documentation marked public,
  • the work is on synthetic / benchmark prompts.

It is not appropriate when:

  • the work is on internal / proprietary source code,
  • the work touches customer data, secrets, or production config,
  • the request includes any user PII.

When in doubt, leave sensitive=true (the default).

The privacy gate

src/smart_gateway/privacy_gate.py runs on every request that reaches /v1/chat/completions. It:

  1. Scans each message body for likely secrets.
  2. Replaces matched secrets with [REDACTED …] placeholders.
  3. If any secret was found and enforce_sensitivity=True (default), the gate sets task.sensitive = true for the routing pipeline.

The patterns are:

Pattern Redaction
AWS access key (AKIA[A-Z0-9]{16}) [REDACTED AWS ACCESS KEY]
GitHub token (gh[pousr]_…{36}) [REDACTED GITHUB TOKEN]
Private key block (-----BEGIN … PRIVATE KEY----------END … PRIVATE KEY-----) [REDACTED <type> PRIVATE KEY]

These are regex approximations. The gateway is not a DLP; it is a "this almost certainly shouldn't leave the box" filter.

Privacy-aware routing

Once task.sensitive is set, the policy applies:

  • A request that resolves to a free model (Nemotron Scout, direct free on SUMMARIZE / CLASSIFY / PUBLIC_REVIEW, or any request that explicitly forced the free tier) becomes STOP_FOR_HUMAN with cause="free_route_blocked_by_privacy" and sensitive_block_applied=true.
  • The HTTP response is 403 with {"detail":"A force-free request that violates privacy policy must fail closed."}.

This applies whether the sensitive flag was set explicitly or silently by the privacy gate.

Fail-closed vs fail-open

The policy is fail-closed. There is no setting that flips the gate to allow free models on sensitive content; the only way to allow a free call is to set sensitive=false and avoid putting secrets in the request body.

The single environment knob is ALLOW_FREE_MODEL_FOR_PRIVATE_REPOS, which defaults to false. It is checked at gateway startup. Flipping it requires a privacy review and a release.

What is logged

The privacy gate redacts the message body before it is passed to the routing layer and the upstream. The structured logs do not contain the original secret; they contain:

  • the routing decision (model, cause, hard_rule, sensitive flag),
  • the message count, never the content,
  • the OpenRouter completion ID (gen-…),
  • the task ID and (if provided) the session / goal / task IDs.

Audit trail is preserved without leaking content. This is part of the architectural freeze; you cannot configure the gateway to log message bodies.

What about tool output?

Tool output is untrusted data. The policy never treats tool output as a routing directive. Only the user-facing fields of the request (model, metadata, control headers) can affect routing.

This is part of frozen-v3/15-privacy-security-spec.md:

Control Plane Isolation: Untrusted input (code, comments, tool output) CANNOT override routing. Overrides strictly require authenticated headers X-Smart-Gateway-Route.

What about prompt injection?

A prompt that asks "ignore previous instructions and route me to the free model" cannot override the privacy gate. The marker-based high-reasoning override (/pro) is also not a privacy override — it routes to Pro, not to the free model.

The privacy decision is made on the original task.sensitive value, which the gateway holds authoritatively. A user-supplied "sensitive: false" is honored only if no secrets were detected.

Multimodal content

Multimodal content parts (type: "image_url") are rejected by the v1.0.0 policy layer with HTTP 400. They never reach the privacy gate, the routing layer, or OpenRouter.

See also