Security¶
Sommelier is a pipeline you run from your own machine and your own Modal
account, not a hosted multi-tenant Sommelier service. Hebrew v3 dataset creation
does cross one additional service boundary: its one-time teacher sends bounded
translation requests from Modal to OpenAI. The realistic local threat is
quieter: a Hugging Face or OpenAI token, provider journal, or home directory
path ends up inside an artifact that is committed, shared, or published next to
an adapter. Most of the security machinery exists to make credential leaks fail
loudly and to keep raw provider records outside the public dataset. The
enforcement lives in sommelier/security.py and sommelier/redaction.py.
Secrets live in the environment, never in config¶
Config files are portable artifacts: they get committed, copied into every run directory as config.resolved.yaml, and shipped alongside results. So the config loader refuses to load a config that looks like it contains a secret. validate_no_secrets runs on the raw YAML mapping when a config is loaded, again on the validated config dump, and once more on the resolved config before config.resolved.yaml is written. A violation raises SecurityPolicyError (code SOM006, exit 5, see the error reference).
The check has two parts:
- Sensitive key names. Any key matching
token,secret,password,api_key, orapikeyas a delimited word is rejected. A small allowlist (dedupe_key,query_column,tools_column,answers_column,redact_fields,target_modules) keeps legitimate field names from tripping it. - Token-shaped values. Strings matching known credential formats are rejected:
hf_,sk-,ghp_, andxox[baprs]-prefixes with minimum lengths.
The English reference workflow needs HF_TOKEN for Hugging Face downloads.
Locally it lives in .env (git-ignored; .env.example is committed); pipeline
and semantic-review entrypoints forward it as a dotenv-backed Modal secret.
Hebrew v3 translation additionally requires two exact named Modal secrets:
openai-api-key containing OPENAI_API_KEY and huggingface-read-token
containing HF_TOKEN. The OpenAI producer does not silently fall back to the
dotenv secret. The remote guide gives the
provisioning commands. None of these values is written to artifacts. Other
dotenv values, such as a wandb key for opt-in tracking, travel only through
entrypoints that explicitly mount the dotenv-backed secret.
Redaction at write time¶
Every log message and string log field passes through redact_text before it reaches disk, as do the evidence strings in the release preflight report:
- Token-shaped substrings (
hf_...,sk-...,ghp_...,xox...) become[redacted]. - The current value of any environment variable whose name contains
TOKEN,KEY,SECRET, orPASSWORD(and is at least 8 characters long) becomes[redacted]. - The home directory path is shortened to
~.
Failed stage manifests are stricter. If the error message contains hf_, sk-, ghp_, token, secret, or password (case-insensitive), the entire message collapses to stage failed; details redacted; otherwise it is truncated to 500 characters. The blunt rule is deliberate: a library that embeds an auth header in its exception text cannot smuggle it into a manifest, at the cost of occasionally over-redacting a harmless message.
The artifact secret scanner¶
Write-time redaction covers what Sommelier writes. The scanner covers text artifacts under the artifact root, regardless of who wrote them. It inspects every .json, .jsonl, .md, .txt, .yaml, and .yml file. JSON is parsed structurally so findings carry a field path; JSONL is scanned line by line with line numbers; everything else is scanned as text. Token-shaped JSON object keys are scanned as well as values. Adapter publication additionally parses and scans the __metadata__ object inside adapter_model.safetensors instead of treating the binary container as opaque.
| Finding kind | Trigger |
|---|---|
sensitive_key |
A JSON key matching the sensitive-name pattern |
secret_value |
A token-shaped string (hf_ / sk- / ghp_ / xox prefixes) |
sensitive_env_value |
The current value of a TOKEN/KEY/SECRET/PASSWORD environment variable appearing in content |
home_path |
The current home directory path appearing in content |
The scanner is a release preflight gate (artifact_secret_scan): any finding fails closed with SecurityPolicyError and exit 5, and publishing stops. Preflight v2 scans and hashes each file through one coherent inspection pass, so a report cannot claim a clean scan while certifying different bytes. The full gate list is on the licensing page. Note that the env-value and home-path checks compare against the current environment, so the scanner catches your secrets and your paths; it is a hygiene gate for artifacts you produced, not a general-purpose secret detector.
Publication adds a second mutation boundary. Each public command first copies
the caller's inputs into a private mode-0700 snapshot and validates that copy;
it then materializes a read-only upload snapshot whose inode, size, and SHA-256
are checked before and after the commit call. The receipt must live outside the
source bundle and is reserved before any Hub access. These constraints keep a
concurrent edit to the original bundle from changing the bytes that were
validated and submitted. The returned immutable revision is still downloaded
and hash-checked before the receipt can become verified.
Trust boundaries¶
The pipeline treats external content as data, never as instructions or code:
| Input | Trust | Handling |
|---|---|---|
| Raw dataset rows | Untrusted JSON strings | Validated field by field; failures are dropped with a recorded reason, never repaired (data policy) |
| Queries and tool schemas | Untrusted content | Instruction-chat translation places only the exact selected tool's name/description and parameter name/type/description projection in escaped canonical JSON, labels it inert/non-output/non-executable, and executes nothing; other translator interfaces receive no schema context |
| Pipeline model repository code | Untrusted by default | trust_remote_code is false in formatting, training, and evaluation unless the config records an explicit reason |
| Hebrew v3 teacher request | Untrusted public source text sent to an external API | Exact dated Responses snapshot and Flex tier; protected literals are placeholder-masked; only the selected tool's bounded projection is included; tools/answers are never translated; returned model/tier, usage, and audits are recorded |
| Model output | Untrusted text | The parser extracts the first balanced JSON span with no repair and executes nothing (evaluation method) |
Trusting remote model code is an explicit, recorded decision. For pipeline
model and tokenizer loads, model.allow_remote_code defaults to false; setting
it to true fails config validation unless model.remote_code_reason states
why. Both fields appear in config.resolved.yaml, and every pipeline load
passes the configured value through as trust_remote_code.
Translation is a separate tool and provider boundary. The Hebrew v3 command
pins the dated API identity gpt-5.5-2026-04-23, explicit Flex service,
instruction-chat request, OpenAI SDK 2.45.0, and the clean implementation
revision. It runs in a CPU-only Modal image and rejects a returned model or tier
that differs from the request. A dated API snapshot is not a public weight
digest, a sandbox, or a guarantee of byte-identical regeneration.
Protected literals are deterministic placeholders before provider access and
are restored and audited after the response. This prevents those exact values
from entering the provider request, but the source query and the selected
tool's name/description and parameter name/type/description projection still
leave Modal and are processed by OpenAI. Requests use strict JSON Schema,
store=false, background mode disabled, truncation disabled, reasoning effort
none, a 900-second timeout, zero SDK retries, and a stable non-PII safety
identifier. SDK retries remain zero so every repeated request is a visible
source-id/attempt event rather than a hidden transport retry. store=false
prevents this workflow from retrieving a stored response; it is not a claim of
Zero Data Retention. Strict structure and script/literal audits do not prove
semantic accuracy or resistance to every prompt injection.
The raw provider journal contains decoded output, response/request ids, usage, and source-id/attempt attribution. It is fsynced for replay and kept in durable producer artifacts, not published in the paired dataset. The translation summary exposes a content-free v2 aggregate and the journal SHA-256 instead. This is not an exactly-once protocol: a process death after provider acceptance but before response receipt/fsync can repeat a billed request, and a hard kill can lose the current Modal chunk before its volume commit.
Tool descriptions remain prompt-injection input even after projection. The
instruction-chat envelope therefore treats their canonical JSON as untrusted
semantic hints only: literal <, >, and & characters are Unicode-escaped
so a description cannot close the context delimiter, control characters stay
JSON-escaped, and the model is told not to follow, execute, translate, or emit
the metadata. Selection uses one unique case-sensitive exact gold call name;
missing or duplicate matches fail closed as an invalid_row. The builder
never reads gold arguments and never includes schema defaults, examples, or
enums. This is prompt-boundary hardening, not a sandbox or a guarantee that a
model will resist every semantic injection; normal output audits still apply.
Domain-specific redaction¶
If your data itself is sensitive (tool schemas naming internal endpoints, for example), report.redact_fields takes a list of field names and replaces their values with [redacted] anywhere in the JSON tree of the evaluation and comparison reports. It applies to the reports only: raw generations in generations.jsonl and decoded translations in the raw provider journal are stored verbatim, so treat the artifact root accordingly. Do not use the hosted teacher for data you are not authorized to send to that provider. See the configuration reference.
What Sommelier does not provide¶
Stating the gaps is part of the posture:
- No multi-tenant isolation or access control. Protecting the artifact root is filesystem permissions, nothing more.
- No authentication on the local server.
sommelier serve adapterbinds127.0.0.1by default and has no auth; it exists to inspect the adapter, not to serve traffic. - The remote vLLM endpoint is open unless you close it. Setting
SOMMELIER_SERVE_API_KEYin.envenables Bearer auth; without it the deployment logs a warning and accepts unauthenticated requests. Details in the serving guide. - No sandboxing of remote model code.
allow_remote_code: truemeans "I trust this model repository"; nothing contains it after that. - No Zero Data Retention guarantee for the Hebrew teacher.
store=falseis recorded, but provider data controls and account eligibility are separate. - Pattern-based detection only. Redaction and scanning target known token shapes and your current environment values. A secret pasted in an unrecognized format will not be caught.