Skip to main content

Data boundary

Where content travels — and what leaves your environment: metadata only.

Two guarantees in every content mode
  • Billing, connectors, spend rows, and logs contain metadata only.
  • Trace content follows ANYRAY_CONTENT_MODE.

How the boundary is enforced

Six independent layers, so one misconfiguration doesn't expose content:

LayerGuards againstControl
Encryption at restContent readable on diskANYRAY_CONTENT_KEY
Content mode & fail-safeContent being kept at all, or silently escalatingANYRAY_CONTENT_MODE, ANYRAY_ALLOW_PLAINTEXT
Content-free spend storeContent leaking into governance dataMetadata-only by design
Trace export gatingContent leaving in exported tracesANYRAY_CONTENT_MODE
Human surfacesHumans seeing content in the UIANYRAY_CONTENT_MODE + RBAC
Admin access controlUnauthorized access to data and settingsANYRAY_ADMIN_TOKEN (+ optional SSO / RBAC)

Encryption at rest

In encrypted mode (the default), content is encrypted with AES-256-GCM before it touches disk, using ANYRAY_CONTENT_KEY (32 hex bytes — openssl rand -hex 32). It fails closed: no plaintext fallback, and the gateway refuses to boot without a valid key. Decryption is an offline, authorized step — the console never decrypts content for display.

Content mode & fail-safe

One org-wide mode, ANYRAY_CONTENT_MODE (default encrypted), decides how much content is kept. Change it at runtime on the console Privacy page; every change is audit-logged with the actor and before/after value.

ModeBehavior
encrypted (default)Content stored as ciphertext (AES-256-GCM via ANYRAY_CONTENT_KEY).
offNo content stored — nothing to decrypt.
plaintextRaw content, readable by admins. Requires ANYRAY_ALLOW_PLAINTEXT=true; debugging only.

The mode only ever degrades toward off, never escalates: without the gate, a plaintext setting falls back to encrypted (if a key is set) or off. See Configure.

Content-free spend store

For spend governance, the gateway writes one metadata row per request — user/team (from x-anyray-metadata), model, provider, token counts (with the prompt-cache split), cost, savings, latency/status. A request routed through a custom provider host is flagged with a boolean (via_custom_route) only — the hostname itself is never stored. No field carries content, not even ciphertext. Rows are retained per ANYRAY_SPEND_RETENTION_DAYS (unset = kept indefinitely).

The same Postgres holds anyray_auth_events — content-free, reason-coded auth/cap rejections for the admin Diagnose view, retained 30 days, never sent to Anyray.

Configured spend connectors write vendor metadata to local anyray_spend_connector_events. These rows never affect anyray_spend, token caps, request governance, or Billing rollups. They expire after 90 days, or sooner when ANYRAY_SPEND_RETENTION_DAYS is shorter, and owner-level GDPR endpoints cover them. Content fields are never imported.

What the Billing app rollup sends

Every deployment sends a content-free Billing rollup every 15 minutes and after health changes. It never includes prompt or response content. The rollup contains:

  • Seats — a distinct active-seat count plus opaque seat hashes (salted pseudonyms; the salt never leaves your deployment).
  • Usage totals — requests, tokens, cost, and month-to-date savings, bucketed by model / provider.
  • Deployment heartbeat — versions, uptime, per-leg health, request/error counts (error class names only), content mode, update method, auto-soft-update state, replica hostname, a count of health reports per anyray-connect version in use, and the names of set ANYRAY_* env vars — values never cross.
  • Optimizer health — hour-by-hour counters per model: optimize outcomes (applied / skipped / timeout / error and normalized reason tokens), hook latency percentiles, per-strategy fired counts and tokens saved, and upstream provider failure counts (429/529/4xx/5xx). Counters and enum tokens only — no per-request data, no user field of any kind.

This powers Anyray-side fleet support ("your deployment has been unhealthy since 09:12") with no access to your environment. Privacy → Data shared with Anyray sets the heartbeat tier (standard default, floor for the mandatory core) and shows the literal last payload that crossed.

Per-user breakdown is opt-in

ANYRAY_METER_PER_USER=true adds a per-user spend + savings breakdown (counts and dollars only, still content-free). Leave it unset to keep the rollup fully pseudonymized.

Optional vendor connector polling

Connectors remain off until an admin adds a credential. In single-tenant mode, security_admin or owner may configure them; in fleet mode, only the deployment owner may do so. The gateway then polls these hosts:

ConnectorOutbound HTTPS host
Cursorapi.cursor.com
Devinapi.devin.ai
GitHub Copilotapi.github.com, then GitHub-issued report downloads pinned to copilot-reports.github.com

Calls send the credential and report window; responses contain metadata stored locally. No inference content is sent. Remove the credential to stop polling.

Support bundle (customer-initiated)

A separate, optional egress for escalation. An admin generates the bundle, reviews the redacted JSON on screen, then separately clicks Send — the gateway regenerates it at send time, so the server, not the browser, decides what leaves. The egress is audit-logged and can never be triggered or pulled by Anyray.

Trace export gating

Traces persist to Postgres inside your environment (anyray_traces / anyray_observations). Content is gated by the mode before it is written: ciphertext in encrypted, omitted in off, raw only in deploy-gated plaintext. Tokens, cost, provider, and attribution are always plain metadata. Traces are pruned at boot and daily — rows older than 90 days (or ANYRAY_SPEND_RETENTION_DAYS) are deleted.

Durable context stash

output_externalize (off by default) can move a bulky tool output off the request and serve it back on demand; observation_mask stashes masked observations the same way. Originals are held in memory by default; the opt-in durable tier persists them in your spend Postgres (anyray_context_stash), gated like the trace store — an AES-256-GCM blob in encrypted, nothing in off. The handle is a content-free keyed HMAC, recall is scoped to the stashing user, and expired rows are swept hourly.

Human surfaces

  • The live /log/stream view shows [REDACTED] for content fields in every mode; request logs are metadata-only even when trace capture is deploy-gated plaintext.
  • Console list views drop request/response bodies entirely.
  • The trace detail view opens to any role with observability:read (viewer+) and shows metadata — model, tokens, latency, cost, optimizer decisions.

Seeing the bodies inside a trace requires observability:read-content (security_admin and owner only); every other role gets a redacted response, not a 403. What they then see still follows the mode: ciphertext in encrypted, nothing in off, readable only in plaintext.

Admin access control

The console and every /admin/* endpoint sit behind admin auth — the break-glass ANYRAY_ADMIN_TOKEN (compared in constant time) and, optionally, per-operator console SSO with RBAC. See Security → Console access and RBAC.