ModelRig Quickstart Routing & reliability Route bundles Probes Bake-offs & replay How it fits Grade protocol Optimization loop Caching lifecycle Bring your traces (OTLP) Observe a pipeline Migration playbook (T0–T2) Recognition playbooks Tenants & statements Published receipts Provenance & trust Artifact content custody The MCP oracle Use-case templates Template: ticket triage Template: document extraction Template: CS next action Template: lead qualification Template: compliance review Template: catalog cleansing Template: call disposition QA Template: financial classification Template: medical classification Leaderboard

Artifact content custody

ModelRig records a metadata graph of the work products your pipeline produces — each artifact's identity, lineage, evaluations, and an integrity hash of its bytes. By default the bytes themselves are never stored: the hash proves what existed, and the content stays wherever you already keep it.

Content custody (E2-C) stores the bytes too — and it is on by default for every new organization (MD-1), because it is what turns your captured prompts, outputs and evidence into analysis over the whole graph. It stays consented and opt-out in one setting: every signup presents the published content-custody terms and records the customer's affirmative acceptance, and any org can dial it back from account settings — the console or the set_org_settings MCP tool — by setting posture off (capture nothing) or metadata (hashes only), or with a zero_retention route. The account posture is the customer control; there is no deployment env var to set.

Managed is the unconditional default

The ratified product direction (Matt, 2026-08-27) is that managed content custody is THE default as every customer signs up — unconditionally, with no operator switch. A new org is created managed, and the acceptance is recorded as an immutable tos_acceptances row (document version + timestamp, context = 'signup') — the lawful-basis artifact, on the same append-only consent log as the ToS clickwrap. The customer can move the org to metadata at any time in settings.

The default lives in the database (migration 00069: the orgs.artifact_posture column default and the signup-trigger fallback both default managed), so it holds for every org-creation path, including magic-link signups. Only the default changes; the isolation guarantees, the PII/PHI scrub floor, the zero_retention carve-out, and the account-settings opt-out (posture off or metadata) are unchanged. Existing orgs are untouched by the migration. The two axes below stay exactly as precise.

What the console cannot do yet. You can see and export every byte in the console; there is no self-service delete button yet — opting out (posture off or metadata in account settings, or a zero_retention route) stops future capture rather than removing what is already stored.

Two independent axes

Custody and telemetry are separate switches. Turning one on says nothing about the other.

AxisQuestionValuesWhere it lives
Telemetry postureIs telemetry captured for optimization at all?optimized · pure_routerorgs.telemetry_posture (00015)
Content custody postureMay artifact bytes be stored, and where?metadata · managed (default)orgs.artifact_posture (00032, default flipped to managed in 00069)

A pure_router org can still enable managed custody; an optimized org can stay metadata-only. Neither implies the other.

Content-custody postures

PostureBehaviour
metadataHash + metadata only. No content object is ever created. The upload-grant endpoint refuses fail-closed (403). The one-setting opt-out from the managed default.
managed (default)Content may be uploaded to the ModelRig-owned object store, isolated under a per-org key prefix ({org_id}/{artifact_id}). Retention is enforced by the store's lifecycle rules per classification. New orgs default here (00069); existing orgs are unchanged by that migration.

Changing posture between managed and metadata is an org-owner action. Adding a posture value is a database migration — a new place bytes may live is reviewed like the authority it is.

How content moves (managed tier)

Content is stored through a posture-gated, consented, write-once path — the bytes never touch ModelRig's control-plane database, and they never flow through the telemetry exporter (which has no path to content, structurally and permanently).

1. Your pipeline saves an artifact. Its metadata row is recorded as always. 2. If the route has opted content on and the org posture is managed and the run is not zeroRetention, the SDK requests an upload grant from POST /v1/artifacts/uploads (a rig_sk_ key with the artifact-write scope). 3. The server authorizes the grant against the artifact's own org (never the caller's assumed org), enforces the size ceiling, and refuses a second upload for the same artifact (content is immutable — it writes once). It returns a short-lived presigned PUT. 4. The SDK uploads the bytes directly to the object store. On any refusal or failure it degrades to metadata-only — a counted, warned skip; it never throws into your run and never retries a deterministic refusal.

Reading content back (the console panel, or an artifact-read key) mints a short-lived presigned GET the same way. Presigned URLs and content hashes are never logged.

On by default, and opt out in one setting

Content custody is on by default — every new org is created managed, having accepted the published terms at signup. The gate is your org content posture (managed → stores; metadata → stores nothing), so the opt-out is always one setting. On the SDK side there is nothing extra to configure for the common case:


# Capture on/off is your ACCOUNT POSTURE, not an env var — the artifact namespace
# is on by default once a control plane (a MODELRIG_API_KEY) is configured, and
# you opt out by setting org posture `off` (capture nothing) or `metadata` (no
# content) in the console or the set_org_settings MCP tool. There is no
# MODELRIG_ARTIFACTS / MODELRIG_ARTIFACTS_CONTENT env gate.
# Scrub mode is ALSO an account/task setting resolved from the control plane —
# off | detect | redact | block, set per task in the console or via
# set_org_settings. There is no scrub env var (MODELRIG_ARTIFACTS_SCRUB was
# removed); a rig with no control plane uses the built-in default (`detect`).
# MODELRIG_CONTENT_UPLOAD_URL        # OPTIONAL — defaults to the ingest host +
#                                    #   /v1/artifacts/uploads; set only to override.
# MODELRIG_CONTENT_API_KEY           # OPTIONAL — falls back to MODELRIG_API_KEY; the
#                                    #   key must carry the `artifact-write` scope.

The SDK attempts upload when the namespace is on and a key is present; the server's org posture decides (a metadata/off org 403s → the SDK degrades to metadata-only). A half-configured transport (no scoped key) degrades the same way — never silently half-on.

Scrubbing (before upload)

When content is on, a deterministic scrub pass runs on the bytes before they are uploaded. Findings are recorded as counts by type, never the matched values — recording a value would move the PII into the control plane, the exact thing custody contains.

ModeEffect
offNo scan; upload verbatim.
detect (default when on)Scan; count findings by type; upload verbatim.
redactUpload a masked copy as a superseding version; the original keeps its own hash and stores no content.
blockIf anything is found, refuse the upload (counted). The artifact keeps its metadata + hash; it stores no content.

Detectors (v1): US SSN (delimited), PAN (Luhn-valid), email, E.164/US phone, labelled MRN.

The same scrub pass also runs on bake-off review samples before they are persisted: a --keep-outputs bake-off scrubs each sample's input and output with the task's mode (detect counts onto the sample's arm_meta.scrub, redact masks, block stores a metadata-only sample), so retained review content never carries un-scrubbed PII either. The mode a sample gets is the rig's per-task content-custody scrub setting, resolved from the control plane (a direct-mode rig has no control plane to consult, so a console-set per-task mode does not reach its samples); a direct-mode rig uses the built-in default — detect when content custody is configured, off otherwise, and samples then persist verbatim. Scrub is an account/task setting, not an env var (the MODELRIG_ARTIFACTS_SCRUB override was removed).

Scrub floor for pii/phi artifacts. An artifact saved with classification: "pii" or "phi" resolves an effective scrub mode of at least redact, regardless of the task's configured mode — so PII/PHI content is never uploaded verbatim just because the mode was off/detect. A stronger block still wins (it refuses the upload entirely). data_classification is a fixed enum — public, internal, confidential, pii, phi — validated at the SDK save gate, at server ingest, and by a DB CHECK; an unknown value is dropped with a teaching warning rather than silently stored.
Deterministic ≠ DLP. These are regex/checksum detectors, not a model. They catch well-formed identifiers and will miss obfuscated or free-form PII. A clean detect result is not a guarantee of no PII. Model-assisted scrubbing is a later version.

Zero-retention still wins

A run marked zeroRetention: true (or a route requiring zero_retention) stores nothing — no metadata, no hash, and no content. The refusal is structural: it returns from the save gate before scrub or any upload grant is even reached. Custody being on does not weaken it.

Retention

Retention is enforced by the object store's lifecycle rules, keyed to each artifact's data classification and prefix — native TTL, not a cron. expires_at on the metadata row records the intended horizon; the store is what deletes the bytes.

What metadata we record, and why it's not content

The attempt envelope (AttemptMeta@v1, on inferences.meta) and the run envelope (RunMeta@v1, on runs.meta) record the metadata that answers analytical questions — what finished how, how much it cost, did the model change under uswithout ever recording the content. Every field is one of five classifications: enum · number · boolean · hash · identifier. There is deliberately no free-text field. This is the structural reason the self-describing analytics API (GET /v1/analytics/meta, POST /v1/analytics/query) is metadata only — enums, numbers, hashes, identifiers; never content: a caller cannot name a column that could carry a prompt or a completion, and the one thing that would be content — the rendered request and the raw output — is stored as a sha256 hash, never the text.

The list below is the same one GET /v1/analytics/meta returns (it is derived from the grammar, so this doc and the endpoint cannot drift).

Attempt envelope (inferences.meta, AttemptMeta@v1)

FieldClassificationWhat it is
finish_reasonenumstop / length / tool_calls / content_filter / refusal / error / other
refusalbooleanprovider-signalled or adapter-derived (documented per adapter)
streamingbooleanwhether the caller streamed deltas
model_snapshotidentifierprovider-reported model id (e.g. gpt-5.2-2026-06-10) — a drift signal, not proof
context_bucketenum≤4k / ≤16k / ≤64k / ≤200k / >200k
error_codeenumcoarse provider error slug (never the message)
regionidentifieronly when the provider reports it
http_statusnumbercoarse transport status
reasoning_tokensnumberthinking tokens, when reported
response_charsnumberlength of the output (not the output)
cost.in / cost.out / cost.cache_read / cost.cache_write / cost.othernumberUSD cost components (omitted when the model is unpriced — never an invented $0)
input_hashhashsha256 of the rendered request (system + user + schema) — a fingerprint, never the text
output_hashhashsha256 of the raw output text
armidentifierthe live A/B experiment arm this call was drawn onto (mode='experiment', LRE-2) — present only on experiment traffic; absent on a pin / non-experiment call, so the row stays byte-identical. Lets the console's A/B readout group outcomes by arm

sampling (temperature / top_p / max_output_tokens, all numbers), tool_calls (declared tool names — an undeclared name is hashed — plus a count), and grounding (citation count + a search-provider identifier) are recorded the same way: numbers, enums, identifiers. A provider_request_id identifier is kept for support correlation.

Reserved dimensions (inferences.tags)

subject, feature, user, session, cost_center are ordinary tag keys (GIN-indexed; no schema change) that the grammar exposes as first-class group-by columns. subject is the customer/tenant the work is for — an opaque customer-side id, hashed by you if it looks like PII. They are business dimensions you attach; ModelRig never derives them from content.

Run envelope (runs.meta, RunMeta@v1)

FieldClassificationWhat it is
pipeline_shahash / identifiergit sha, auto-filled when available
config_hashhashhash of the run's config
triggerenumcron / api / manual / replay / other
actoridentifieran opaque identifier, not a display name — a free-text actor is dropped, counted, and warned
input_hashhashsha256 of the run input

Both envelopes are sanitized at production (the SDK producer) and at the boundary (server ingest for the wire, buildRunMeta for the run): any string that is not an enum/identifier/hash — too long, or containing whitespace — is dropped and counted. A property test feeds content-shaped strings into every field and proves none survive, in the attempt envelope, the run envelope, the wire, the OTLP span attributes, and the /query result. Zero-retention still wins: a zeroRetention run records no envelope at all.