Skip to content

Threat model: security and privacy

Scope: Ratchet v0.4.0 in live mode, made up of the gateway, the SQLite ledger, the Night School driver, the Nebius bucket, the vLLM endpoint, the Token...

Scope: Ratchet v0.4.0 in live mode, made up of the gateway, the SQLite ledger, the Night School driver, the Nebius bucket, the vLLM endpoint, the Token Factory calls, and the Scout demo with Tavily search, as hosted on the demo VM (DEPLOY_NEBIUS_VM.md). Mock mode handles only synthetic data. Each threat below says plainly what exists today and what is planned.

Asset Where Why it matters
Customer request text examples.messages, night/train.jsonl in the bucket, adapters (memorised) May contain personal or confidential data
Teacher outputs (training data) examples.completion, bucket Business value, and license provenance
Student adapters Bucket /data/adapters/* The product the customer owns
Ledger RATCHET_DB Provenance, version state, audit trail
API keys NEBIUS_API_KEY, RATCHET_STUDENT_KEY, TAVILY_API_KEY, RATCHET_ADMIN_TOKEN, RATCHET_API_KEYS, RATCHET_ADMIN_KEYS, Nebius CLI and S3 credentials Spend, data access, and (admin keys) what the gateway distils
BOM signing key RATCHET_BOM_SIGNING_KEY Whoever holds it can sign a bill of materials that claims any provenance
Demo budget Scout’s daily spend and Tavily credits Real money and free-plan quota on a public URL
Release decisions versions.state Control which model answers users
[Client app] --(1)--> [Gateway + ledger host] --(2)--> [Token Factory]
| \--(3)--> [vLLM Serverless Endpoint] <--(5)-- [Bucket]
\--(4) cron: ratchet.live --> [Serverless Job] --(5)--> [Bucket]
  1. Client to gateway: API keys on /v1/* (RATCHET_API_KEYS; off by default in mock mode, required to start in live mode unless RATCHET_ALLOW_UNAUTHENTICATED=1), admin keys on management writes and admin or dashboard keys on the dashboard API in live mode. Plain HTTP unless you add a TLS proxy (Caddy on the demo VM).
  2. Gateway to Token Factory: HTTPS with a bearer key.
  3. Gateway to vLLM: HTTPS with a bearer token (--auth token), if the endpoint is configured that way.
  4. Cron host to the Nebius CLI: local CLI credentials.
  5. Job and endpoint to bucket: a Nebius-managed mount.
  6. Gateway to Tavily (live Scout only): HTTPS with a bearer key. Search results are untrusted web content that flows into agent prompts.
  7. On the demo VM, internet to Caddy: HTTPS. Caddy is the only thing that can reach the gateway’s port 8000.
# Threat Likelihood / impact Present today Planned
T1 Unauthenticated gateway use. Anyone who can reach the port can spend teacher credits and read /ratchet/stats (client names and spend, events), the BOMs, the cost report, task specs and /openapi.json High / High RATCHET_API_KEYS (v0.4): named keys required on /v1/* (401 otherwise), compared as SHA-256 digests with hmac.compare_digest; only the name is stored. RATCHET_API_KEY_DAILY_REQUESTS caps each key per UTC day (429). Since v0.5: live mode does not start without API keys unless RATCHET_ALLOW_UNAUTHENTICATED=1 (for a gateway Caddy or a private network guards); every route has an access level in one table (ratchet/access.py), unknown routes counting as private; in live mode the dashboard API (/ratchet/stats, /ratchet/cost, /ratchet/redact, GET /ratchet/tasks, /ratchet/replay (one version’s aggregates, never request text), the BOMs) needs an admin or read-only dashboard key (RATCHET_DASHBOARD_KEYS; 401, 403 for an API key) unless RATCHET_PUBLIC_DASHBOARD=1, and RATCHET_PUBLIC_BOM=1 opens only the BOMs; an anonymous read of a public live dashboard shows client pseudonyms (client-1, …), also in budget events, events without URLs or error messages, and no dollar caps, neither a key’s nor the gateway’s budget (RATCHET_SCRUB_STATS); /docs and /openapi.json are off in live mode (RATCHET_OPENAPI). Mock mode stays open. The dashboard page cannot send a key yet, so a private live dashboard is read through the API. Since v0.5 each key can also have US dollar budgets, rate limits and a task scope (RATCHET_KEY_LIMITS, see T11), so a leaked key spends at most its own budget on the tasks it was given Per-key token budgets, a key prompt in the dashboard page
T2 Data poisoning via prompts. An attacker sends crafted requests, the teacher answers, and those answers become training data. The attacker controls the prompt, not the label, but can skew the data distribution, or plant prompt-injection text that makes the teacher output harmful labels. Since v0.5 the client’s generation parameters reach the serving teacher, so sampling fields (logit_bias of +100 forces a token) could also steer the label Medium / High The teacher writes every label; the student’s own outputs are never training targets. A served answer written under a client’s logit_bias, a non-zero penalty, a temperature above 1 or a vendor field is never stored (GenParams.unstorable); audits and arbitrations run at temperature 0 without those fields. A request for n choices stores only the teacher’s choice 0, so a client cannot pick which of its choices becomes the label, and a request still adds or bumps at most one example. The conditions that are stored (stop, response_format, tools, tool_choice) are recorded with the example, and every variant of a prompt shares its golden or train split. A fixed golden split and release gates catch broad quality drops. Per-tenant task names keep datasets separate Per-tenant datasets and adapters, anomaly detection on new examples (bursts from one client, near-duplicates), a cap on examples per client, optional human review of the golden set
T3 Backdoor through rare triggers. Poisoned data affects only inputs with a trigger phrase, so golden and audit precision stay high Low / High Audits sample live traffic at random, but a rare trigger may never be audited Trigger scanning, higher audit rates on low-frequency patterns, provenance per client
T4 PII in logs and training data. Escalated and audited requests are stored and trained on; adapters can memorise text High / High Pattern-based redaction before add_example, on by default (RATCHET_REDACT_PII, v0.4): emails, phones, cards, IBANs, Aadhaar, SSNs, PANs, IPs and API secrets become placeholders in both the request and the teacher answer: message content, name, an assistant’s refusal and tool and function call arguments (v0.5). Message keys Ratchet does not use are dropped before storage, never stored raw. Names and addresses are not detected. The teacher still sees the original. Storage opt-outs (v0.5, FR-13.1): per request (store: false, X-Ratchet-Store: none, metadata.ratchet_store), per API key (store=0), per task, per end user (no-train) and zero retention (RATCHET_STORE_EXAMPLES=0); none can be undone from a request, and opting someone back in needs a key. End users are kept only as a 16-hex HMAC scoped to the API key (FR-13.2), so the operator cannot link one person across clients or deployments; the generated key lives in the ledger, so a stolen database file lets an attacker test guessed identifiers against it unless RATCHET_END_USER_KEY is set outside the file. Erasure by client, end user, hash or stored text (v0.5, FR-13.3) removes the text for good and tombstones its hash so it is never stored again; retention windows expire examples and fold old request rows (FR-13.4). Unlearning is out of scope: an adapter trained on erased text may still reproduce it. Ratchet lists those versions (affected_versions, exact when a version committed its training set, otherwise a timestamp approximation that errs toward listing) and trains the next one without the rows, but cannot edit an existing adapter; retiring it is the operator’s decision. Backups made before an erasure still hold the text. The gateway’s logs, metrics and failures rows (v0.5) hold ids, names, numbers and statuses only, never message or answer text, URLs, exception messages or keys; a client’s request id is kept only when it matches ^[A-Za-z0-9._-]{1,128}$, and metric labels are bounded (registered task names, key names) NER-based detection (Presidio) for names and addresses, Token Factory zero-data-retention setting, excluding fields marked sensitive, signed erasure certificates and a forced retrain (FR-16.9)
T5 Key leakage (NEBIUS_API_KEY, RATCHET_STUDENT_KEY) Medium / High Keys are read from the environment only. .env is in .gitignore A secrets manager, separate keys for gateway and Night School, spend alerts
T6 License misuse. Training on outputs from a teacher whose terms forbid it Medium / High licenses.require_distillable() refuses closed teachers (gpt, o1, o3, o4, claude, gemini prefixes) and unknown models at startup, the fallback teacher (RATCHET_TEACHER_FALLBACK_MODEL) included. Every example stores the teacher that wrote it and that teacher’s license (a fallback teacher’s answers only with RATCHET_STORE_FALLBACK_TEACHER=1). dataset.rows() drops non-distillable rows. /ratchet/bom exports provenance. Each adapter gets its own bill of materials at training time, signed with Ed25519 when RATCHET_BOM_SIGNING_KEY is set (v0.4); python -m ratchet.bom verify detects any edit. Since v0.5 each BOM signs sparse Merkle roots over its exact training and golden examples, each leaf binding the example’s label, teacher, license and redaction count, so one example’s license can be proved without the dataset (python -m ratchet.proof), and live mode writes the BOM into the adapter’s folder in the bucket. The BOM also names the base model’s license and carries the exact NOTICE.md written next to the weights; live Night School puts both in the adapter’s bucket folder and python -m ratchet.live export bundles them (v0.5). Teachers added by configuration (RATCHET_TEACHER_LICENSES) must use an allowed open license and may not name a closed model family, closed model name or closed vendor Ratchet recognises (closed_family), or a router alias (openrouter/auto), so the environment cannot relabel a known closed model; the BOM and NOTICE mark configured teachers config A license registry kept up to date with model cards, and legal review per teacher. Any id the check does not recognise is admitted on the operator’s claim alone: a wrong open license for an open model, or a closed model from another vendor under its own name (x-ai/grok-4), gets through RATCHET_TEACHER_LICENSES; the config marks make such claims visible to whoever receives the adapter. Not legal advice
T7 Token Factory terms §5(f). “Develop a product or service that competes with the Service” Unknown / High Written clarification requested from Token Factory support (Oct 19, 2026). Positioned as running on Nebius end to end Act on Nebius’s answer
T8 Student endpoint exposed without a token, or with a weak one Medium / Medium student_endpoint.sh requires RATCHET_STUDENT_KEY and sets --auth token Rotate keys, private networking
T9 Unauthorised runtime LoRA loading. VLLM_ALLOW_RUNTIME_LORA_UPDATING=True lets any caller with the endpoint token load adapters from paths the server can read Medium / High Protected only by the endpoint token Separate admin path or proxy, an allow-list of adapter paths
T10 Release state tampering. Writing to the ledger changes which model serves users; so do the runtime switches (turn the student off, freeze releases, pin a task, set the audit rate to 0 so no canary is ever judged), and two writers racing (the gateway’s tick, the cron tick, an operator) could promote twice or leave two versions live Low / High File permissions on the host. Since v0.5 manual changes go through one module, from the admin API (every /ratchet/admin/* route, reads included, needs an admin key from RATCHET_ADMIN_KEYS: 401, 403 for an API or dashboard key, off in live mode without admin keys) or the CLI on the host (cli:<OS user>); every change needs a reason, is checked against an explicit table of moves (nothing becomes live except a canary, a version without a golden eval is never promoted, a rejected or rolled-back version returns only with force, an audit rate of 0 during a canary only with force), and writes an admin_actions row (actor name, never a key; reason, redacted; state before and after; the call’s request id) and an admin event in the same transaction as the change. Every release move is compare-and-set inside BEGIN IMMEDIATE, so concurrent writers make each decision once and at most one version is live and one in flight. A rollback and the live alert run even while releases are frozen. Signed or hash-chained state changes (a tamper-evident ledger) so a raw SQL edit on the host is detectable; two-person approval for promotions to live
T11 Denial of wallet. Long prompts, or many escalations to the teacher Medium / Medium Request limits on the gateway (v0.5), checked before any model call: body size (RATCHET_MAX_BODY_BYTES, 2 MiB, 413, also for chunked bodies), messages per request (RATCHET_MAX_MESSAGES, 256) and characters of input (RATCHET_MAX_INPUT_CHARS, 120,000, 400 context_length_exceeded), counting every string forwarded to the teacher (content, name, tool call arguments and ids); message keys Ratchet does not use are dropped rather than forwarded, and non-text parts (only with RATCHET_TEACHER_MODALITIES) are bounded by the body size alone; n is at most RATCHET_MAX_N (4; v0.5): each choice past RATCHET_SAMPLES costs one more student sample, and an escalation asks the teacher for n answers, so the cap bounds what one request can multiply; RATCHET_MAX_N=1 refuses n above 1 as before. A refused request costs nothing and uses no quota. Per-key daily request quotas (RATCHET_API_KEY_DAILY_REQUESTS, T1) and Token Factory rate limits. Teacher retries are bounded (v0.5): two at most within 180 s, a read timeout (whose tokens may be billed) once, never a 4xx the same request would get again; and X-Should-Retry: false on errors a retry cannot fix stops OpenAI SDKs from resending a request whose student samples would run again. Output is capped per call (v0.5): every teacher call carries max_tokens of at most RATCHET_TEACHER_MAX_TOKENS (8,192) and every student call at most RATCHET_STUDENT_MAX_TOKENS (1,024), whatever the client asks (a larger value is capped, none means the cap); the reasoning headroom stays under the teacher’s cap. Allowlists (RATCHET_TEACHER_PARAMS, RATCHET_STUDENT_PARAMS) decide which client fields reach each model, so a client cannot raise n or pick the model, and unknown vendor fields go nowhere unless named. Spend and rate limits (v0.5): per key, US dollar budgets per UTC day and month (RATCHET_API_KEY_DAILY_USD, RATCHET_API_KEY_MONTHLY_USD, per key in RATCHET_KEY_LIMITS; 429 budget_exceeded), counting what failed requests spent, requests a minute and in flight (429 rate_limit_exceeded) and a task scope (403); for the whole gateway a daily /v1 budget (RATCHET_DAILY_BUDGET_USD) that blocks every chat request or, with RATCHET_BUDGET_MODE=student_only, only the ones that need the teacher, so escalations stop costing while confident student answers go on; budget events at 50, 80 and 100% of every cap (RATCHET_BUDGET_ALERT_PCT); a per-IP rate on a gateway without keys (RATCHET_IP_RPM, 120 a minute in live mode); and at most RATCHET_MAX_INFLIGHT (32) chat requests in flight, past which the gateway answers 503 at once instead of queueing, so a burst cannot tie up every worker thread. A request takes its in-flight slots only once its body has arrived, so slow uploads (nothing times a body out) cannot fill the cap and refuse everyone else. The counts are kept in memory and read from the ledger at startup, so neither a restart nor the quota check itself (no query per request) is a way around them. A request in flight can overshoot a cap by its own cost, and concurrent requests by theirs. A response_format schema is checked on the request path (v0.5): its size and depth are limited at validation, a client’s pattern is never run, one check stops after jsonschema_lite.MAX_STEPS steps (about 0.3 s; an answer that cannot be checked counts as a mismatch), and each distinct answer is checked once per request; a schema repair adds at most one teacher call per request, never for n > 1 or a cut-off answer, and RATCHET_SCHEMA_REPAIR=0 removes it. A tool request (v0.5) costs one teacher call and no student samples, so offering tools or sending tool turns cannot multiply a request’s calls; it can only keep that request at the teacher’s price, which the quota already bounds, and tool_requests per task in /ratchet/stats shows how much traffic takes that path Per-key token budgets, escalation-rate alerts
T12 Task registry abuse. Every new X-Ratchet-Task value adding an entry to the in-memory registry; a client inventing task names whose answers become training data; a client using another tenant’s task (its labels, its spend); a client turning distillation on or off for a task others share; anyone redefining a task’s rules Low / Medium Fixed in v0.1.1: unknown names get a temporary task that is not registered; names must match [A-Za-z0-9_.-]{1,64}. Since v0.5: tasks are registered only by an operator (the code, RATCHET_TASKS_FILE, or the admin API behind RATCHET_ADMIN_KEYS, which must differ from every API key; off in live mode without them; 401 or 403 otherwise; the admin’s name, never the key, is stored and logged), at most 500 in all (50 API tasks in mock mode); RATCHET_UNKNOWN_TASKS=teacher or reject stops unregistered names from becoming training data (and answers a header that is not a valid name with a 400 instead of default); a task’s clients allow-list answers 403 to any other key, and to a request without one, before any quota or model call; only an operator’s spec, or an admin’s runtime pin (/ratchet/admin/tasks/{name}/pin, T10), can pin a task, never a header, metadata, model or response_format; since FR-14.2 a request’s response_format json_schema registers an auto task when no registered name is given (under teacher and reject only for a request that names no task), which is distilled: never pinned, its fields derived from the schema, at most RATCHET_MAX_AUTO_TASKS (200; 0 turns auto tasks off, so a schema request keeps the task it names, or default), never stored when its derived spec holds personal data, and replaceable or deletable through the admin API; a schema with nothing comparable gets no auto task; an auto task serves only a schema that derives the same fields (a client naming another client’s auto task, or squatting its computable name first with a weaker schema, gets its own schema’s task instead; a client’s own auto- name never names a new task; 16 hex digits of SHA-256 make forcing another schema’s name impractical; auto_task_collision events), so no client chooses the fields another client’s answers are compared on; a teacher refusing a client’s schema turns json_schema off for no one else, since the endpoint must refuse a minimal probe schema too; a spec is checked whole (unknown keys refused, every value range-checked, bounded nesting, strict JSON) before it is registered, and fixture paths cannot leave RATCHET_FIXTURES_DIR Per-tenant registries; task changes in the admin_actions audit table too (runtime pins already are, T10)
T13 Judge manipulation. Candidate output contains instructions to the judge Low / Medium The judge is used only for free-text tasks and does not gate releases in v0.1. Labels are compared exactly Keep exact-match fields as the gate; delimit candidate text in the judge prompt
T14 Dashboard XSS from event or license text, or a JSON answer a browser sniffs into a page Low / Medium dashboard.html escapes all dynamic text with esc() and opens links with rel="noopener"; React escapes text it renders. Since v0.5 every response carries X-Content-Type-Options: nosniff and Referrer-Policy: no-referrer, and RATCHET_CSP sends a Content-Security-Policy (opt-in, so the pages never break by default; a recommended value that the React build and the lite pages work with is in CONFIGURATION.md). Scrubbed reads replace URLs in event details with <url> A CSP without 'unsafe-inline' once the pages’ inline scripts move to files
T15 Prompt injection from web search results into Scout’s agents. A page Tavily returns contains text like “ignore your instructions and…”, aimed at the triage, reader or writer agent Medium / Medium Results are only data: titles, URLs and snippets go into a JSON user message, never into a system prompt. Triage and reader outputs are labels (relevant, source_type, answers_question, fact_type) plus one short free-text field that is ignored for agreement and training comparison. The writer is pinned to the teacher, so injected text cannot steer which model answers it. No agent runs tools or code from model output: the only tool is search, called with the planner’s queries. The Scout page escapes all text and only links http(s) URLs, opened with rel="noopener noreferrer" Delimit untrusted text more strictly in prompts; cap excerpt length further; flag answers that cite a source the reader marked as not answering
T16 Poisoned training data through Scout. Injected search text makes the teacher write wrong labels, which become training data for source-triage and page-read Low / Medium Only label fields are compared; the release gate and audits catch broad quality drops (see T2). Pinned planner and writer answers are never stored Same as T2
T17 Budget exhaustion on the public demo. Someone runs Scout over and over to spend Token Factory credit and Tavily credits High / Medium Budget guard in live mode: RATCHET_SCOUT_DAILY_BUDGET_USD (default $3.00 per UTC day), RATCHET_SCOUT_MAX_RUNS_PER_DAY (default 30, about 90 Tavily credits), RATCHET_SCOUT_RUNS_PER_HOUR per IP (default 6). Spend is counted per step, so failed runs count. Questions are capped at 300 characters. Worst case per day is the cap plus the runs in progress when it is reached. Since v0.5 the daily counters are kept in the ledger and restored at startup, so a restart or an update no longer resets the cap; many IPs can still use up the public share early (a denial of service for others, not extra cost), but a judges’ reserve of runs and spend (RATCHET_SCOUT_JUDGE_RUNS_PER_DAY, RATCHET_SCOUT_JUDGE_BUDGET_USD) is held back from the public, with the largest finished run kept as a margin so public runs in flight cannot spill into it. Tavily credits are metered per search against RATCHET_TAVILY_MONTHLY_CREDITS and paced per UTC day until RATCHET_TAVILY_PACE_UNTIL, so the public cannot spend the month early; a run without credits runs without search, labelled, still bounded by the run and USD caps Per-user quotas behind a login; CAPTCHA
T18 Shared mock demo controls. Any visitor can run days, break the next deploy or reset the shared simulation, or flood the controls; without admin keys the mock admin API is open too (as mock-admin), so a visitor can turn the student off, freeze releases or roll a canary back for everyone Medium / Low Mock mode only, no real cost. day and reset run one at a time (409 when busy); reset has a 30 s cooldown (429); requests is clamped to 100–5,000; a reset clears every switch, pin and admin action (a new in-memory ledger), and every admin action is in the audit log and the release log. RATCHET_API_GUARD=guarded blocks /ratchet/demo/* and /ratchet/admin/* at Caddy; RATCHET_ADMIN_KEYS closes the admin API in mock mode too Per-visitor simulations
T19 Public paid API on the demo URL. /v1/chat/completions is open on the VM, and the Scout budget guard does not cover it High / High in live mode Opt-in Caddy guard: RATCHET_API_GUARD=guarded returns 403 on /v1/* and /ratchet/demo/* without X-Ratchet-Admin-Token; a token shorter than 32 characters blocks everyone; Caddy strips the header before the gateway. Gateway API keys (RATCHET_API_KEYS, see T1) work with or without it. Both are off by default, but since v0.5 a live gateway refuses to start with neither: without API keys it needs RATCHET_ALLOW_UNAUTHENTICATED=1, which the VM’s .env sets only for a Caddy-guarded demo. Caddy logs request headers and redacts only Cookie, Set-Cookie, Authorization and Proxy-Authorization, so since v0.5 the Caddyfile’s log filter deletes X-Ratchet-Admin-Token from the access log (check with caddy validate on the VM). Since v0.5 a live gateway without keys limits each client address to RATCHET_IP_RPM (120) requests a minute (uvicorn takes the address from Caddy on the VM); RATCHET_DAILY_BUDGET_USD caps what /v1 may spend in a UTC day (with keys, each key’s own budget too), and budget events at 50, 80 and 100% reach the log A per-IP spend cap; the dashboard page showing the budget
T20 Spoofed client IP to dodge the per-IP limit Low / Low On the VM, Caddy overwrites X-Forwarded-For and uvicorn trusts it only because port 8000 is not published. RATCHET_TRUST_PROXY is off by default Keep port 8000 private
T21 Stolen BOM signing key. Someone signs a BOM that claims clean provenance for an adapter trained on anything Low / High The key is only read from the environment and never logged or returned; /ratchet/bom/public-key returns the public half only. Each BOM names its key id, so a rotated key can be listed as revoked Keep the key in a secrets manager; sign on a separate machine instead of the gateway
T22 Misleading cost records. Someone records the student endpoint as down while it runs (or up while it is not), so the cost report hides or invents GPU cost and recommends the wrong thing Low / Medium POST /ratchet/cost/endpoint needs an admin key (RATCHET_ADMIN_KEYS; off in live mode without them); every change writes an endpoint event with its source and the admin’s name; the API and CLI open and close intervals and never delete one; the report labels prices unverified and the runbook reconciles endpoint hours with the Nebius bill weekly Read uptime from the Nebius API instead of trusting records
T23 Training-set membership oracle, and proofs read as more than they are. POST /ratchet/versions/{id}/proof answers whether a request was trained on: for a text someone can guess (a known customer’s ticket, a leaked prompt) that is a privacy leak. A non-membership proof, and so a held-out certificate, also names the leaf next to the key’s path (another example’s key and label hashes). And a reader may take a membership proof as evidence about the weights Low / Medium The proof and held-out endpoints need an admin key (RATCHET_ADMIN_KEYS; off in live mode without them; 401 or 403 otherwise), and the sample endpoint is mock-only. A proof holds hashes only: keys are hashes of redacted text, values hashes of the label, teacher, license and redaction count. The stored leaves are append-only (INV-17), and the gateway refuses (500, one integrity event) to serve a proof from leaves that no longer rebuild the signed root. The API guide and the CLI say what a proof shows: the operator’s signed commitment, at training time, to the dataset the adapter was trained and graded on; it does not show that the weights came from that dataset Per-tenant proofs (a client asks only about its own examples), a rate limit on proof requests, and a training job that recomputes the root from the rows it reads and writes it beside the weights
T24 Feedback and import poisoning. A client with a key uses POST /v1/ratchet/feedback to write wrong labels as corrections, to overwrite another client’s or the operator’s labels, or to store an opted-out end user’s prompt; an operator imports a closed model’s outputs, or a teacher’s outputs as human labels Medium / High A key reaches its own requests only (another key’s request is the same 404 as an unknown one); one stored correction per request, so the daily request quota bounds them; a correct verdict stores nothing, so the student’s own text never becomes a label (INV-2); every correction is validated against the task’s spec, redacted and kept out by the request’s recorded opt-out, the opt-outs in force and pinning; a request that named an end user needs every identifier it sent (both user and safety_identifier when it sent both; the request row keeps a keyed hash of each to check them), so every no-train registration of that end user, made with either identifier or by an admin for every client, applies; a client’s correction never replaces another client’s or an import (teacher < correction < import); RATCHET_FEEDBACK_CORRECTIONS=0 keeps verdicts and stores no correction; human precision never promotes or rolls back. Imports: --source teacher:<model> must pass require_distillable and no closed family (closed_family), with the clause quoted; human labels need an attestation, recorded in an import event with the file’s SHA-256; a Ratchet train.jsonl row whose provenance names another source is refused, so teacher outputs cannot be relabelled human; human can never be configured as a teacher. Golden-only rows cannot be prompts already trained on Per-client caps on corrections, review of corrections before they reach a golden row, anomaly detection on verdicts from one client. A correction’s messages are the client’s word (request rows hold no text to compare them with), so a client can label prompts it never sent; it could send them as requests anyway. Likewise a client can paste the student’s own served answer as the “right” answer with an incorrect verdict and it is stored as that client’s human label: the gateway keeps no digest of the served text to compare with. That is a client attesting a label, not the system training on student output (INV-2), and the bounds above apply
T25 Savings receipts read by the wrong party, or read as more than they are. A receipt’s leaves list every request’s time, task, route and cost; request ids are the ledger’s global row ids, so a client that subtracts two of its own ids, or reads first_id, last_id and n on its receipt, learns how many requests other clients sent in between (the gateway’s total volume, not who sent them). And a reader may take a signed receipt as proof of the costs themselves Low / Medium /ratchet/receipts* need a key when keys are on: an API key reads only its own receipts, proofs and leaves (403 for another client’s), an admin key every one; live mode without any keys serves none (403). Leaves hold metadata only, the key’s name and never the key (INV-12, INV-13). Receipts are written once (INV-17), chained per client and signed with the BOM key; the gateway refuses (500, one integrity event) to serve a proof or leaves from rows that no longer rebuild the signed root (410 when request retention folded them, which is no tampering). The runbook says what a receipt shows: edits after sealing are detectable and the baseline is calibrated on audited teacher bills; costs written at request time are not verified, so the runbook reconciles teacher spend with the Token Factory bill Proofs taking the random ratchet.request_id and mapping it back to the row, so the row id (ratchet.row_id, X-Ratchet-Request-Id) need not be handed out

| T24 | Judge code leak or guessing. The code that unlocks the judges’ reserve (RATCHET_SCOUT_JUDGE_CODE) is guessed, or leaks through a log, a URL, a page or the video, and the public spends the judges’ share | Low / Medium | At least 16 characters (openssl rand -hex 16 gives 32); sent only once, in a POST /scout/judge body, never in a URL or a custom header, because uvicorn and Caddy log both (Caddy redacts the Cookie header by default); compared as SHA-256 digests in constant time; 10 attempts per IP per rolling hour (429); never stored, logged, echoed or shown in a settings error (only its digest and a derived key are kept). The session is a signed HttpOnly, SameSite=Strict, Secure cookie keyed from the code, so another site cannot start a judge’s runs through /scout/stream (a GET), and rotating the code revokes every session. The code goes in the submission’s testing instructions only, never on the public project page or in the video. Even a leaked code can only spend the judges’ share: the daily totals still hold | Per-judge codes; a code that expires after judging | | T23 | Readiness as reconnaissance or a load amplifier. /readyz is public: it tells anyone which dependency is down, and if every call reached the teacher, polling it would spend the teacher’s rate limit (or tokens) | Low / Low | v0.5: the answer names states, statuses and exception classes only, never a URL, a key, a model list or an exception’s message (live mode reports the number of config warnings, not their text); the teacher and student probes are GET /models, which spends no tokens, and each probe’s result is cached for 15 s and shared by every caller, so a dependency sees at most one probe per 15 s however often /readyz is asked; probes run on their own three threads, so polling cannot take request threads. A misconfigured gateway stops at startup instead of serving with a wrong key or an unknown teacher (config.check), and messages there never repeat a key’s value | Serve /readyz on an internal port only, or behind the Caddy guard |

  • Mock mode and the simulation use only synthetic tickets (sim/tickets.py) and simulated search results on example.org (sim/research.py). The repo contains no real customer data.
  • In live mode, Scout sends its search queries (written by the planner from the user’s question) to Tavily under Tavily’s terms. Do not type personal data into the public demo.
  • Scout questions appear in the uvicorn and Caddy access logs, because /scout/stream?q= carries the question in its URL (EventSource can only send a GET). They are public demo questions; the logs rotate (10 MB × 5 per container). The ledger stores no question and no client IP: Scout’s spend rows hold only the pool, amounts and outcomes.
  • In live mode, requests that reach the teacher are also processed by Nebius under its terms. Token Factory stores inputs and outputs for speculative decoding unless you opt out (ToS §7). Opt out for customer data.
  • Stored examples have identifiers replaced by placeholders (RATCHET_REDACT_PII=1, the default). /ratchet/redact shows what would be stored for any text and stores nothing. The redactor does not find names or street addresses.
  • Requests the student serves without an audit are not stored as examples, but their metadata (task, confidence, cost) is logged in requests. Message text is not stored in requests. The same holds for failures, for the gateway’s log lines (one per chat request in live mode) and for /metrics, which an operator ships to log and metrics collectors.
  • A client or an end user can keep requests out of the training set (store: false, a no-train registration; API.md), and an operator can turn storage off entirely (RATCHET_STORE_EXAMPLES=0). The user and safety_identifier fields are never stored or forwarded: only a keyed hash is.
  • What was stored can be erased (an end user’s examples by the client itself, DELETE /v1/ratchet/examples; anything by an operator, POST /ratchet/admin/privacy/erase or python -m ratchet.admin erase), expires with RATCHET_EXAMPLE_RETENTION_DAYS, and can be exported with its provenance. An erasure record holds hashes, counts and a redacted reason, never the text or a raw end user; erased hashes stay as tombstones, so anyone holding a candidate text can check whether it was erased. Adapters already trained on erased text are reported, not changed (API.md).

See the repository’s SECURITY.md for how to report a vulnerability.