uv run translucent serve --ledger <ledger-dir> --state <state-dir> # http://127.0.0.1:8000The ledger must already exist (a mistyped path is refused); add --create-ledger to start on a new, empty one.
verify, experiments, export, resume and mcp likewise refuse a path that holds no ledger.
The intervention command runs locally through the existing metered providers.
The meta planner is Muse Wonder (muse-wonder-1.0); dev uses Muse Spark Open
on Fireworks. Credentials stay in the environment used by the provider client
(MODEL_API_KEY or META_API_KEY, or FIREWORKS_API_KEY for dev). No keys are
needed for estimates or replay.
uv run translucent intervene nist --profile meta --budget 25 --split loco --dry-run
uv run translucent intervene nist --profile meta --budget 25 --split prequential --dry-run
uv run translucent intervene nist --profile meta --budget 25 --split loco --ledger /tmp/nist-live --recordings /tmp/nist-live-recordings
uv run translucent intervene nist --muse-only --profile meta --budget 10 --split loco --ledger /tmp/nist-muse-only --recordings /tmp/nist-muse-recordings
uv run translucent intervene nist --profile meta --budget 25 --split loco --ledger /tmp/nist-replay --replay /tmp/nist-live-recordings
uv run translucent intervene nist --muse-only --profile meta --budget 10 --split loco --ledger /tmp/nist-muse-replay --replay /tmp/nist-muse-recordingsUse fresh ledger and recording directories for each run. Copy the entire private
recording directory to replay on another machine with the same source checkout.
The profile, budget, split, step cap and other recorded settings must match.
--recordings defaults to <ledger>/recordings; the omitted --ledger default
creates a uniquely named directory under state/. --steps sets the per-fold
tool-call cap (120 by default); --split also accepts lofo and prequential.
The live planner records three competing mechanistic hypotheses before registering
strategies. prepare_training batches unscored training commitments and reveals;
execute_fold executes the submitted, screened specs through individual
commit/observe/reveal records. These tools reduce conversation overhead while the
controller still checks every executable prediction and paid window. LOCO/LOFO
strategies stay frozen, and each new fold gets fresh model history and seals.
Muse-only uses one tool-free call per held-out trial and one additional call if it
chooses a 30 or 60 second observation. Its input is the declared metadata and a
numeric temperature table with at most 128 evenly spaced samples, including the
available endpoints. The second call includes only the paid post-off window.
Its completed campaign records scored muse_only baseline results in place of an
unrun stub. The earlier scripted benchmark remains unchanged; independently run
campaigns are not silently pooled into its comparison family.
Every attempt is recorded with request/response digests, model, usage and cost, including retries, finish reasons, reasoning tokens and refusals. Recordings publish atomically, and paid spending is retained if recording fails. The campaign cap reserves a conservative request ceiling before contacting the provider, in addition to the existing global provider cap and pause switch. Estimates state token assumptions; they exclude retries and repair replies and do not promise completion within a given budget. Exit code 0 means completion; a clean budget, step, refusal or stalled stop returns 2 with its recorded reason. A budget stop during a watch can leave an unfinished episode: no final judgment or outcome is invented, and such a ledger cannot support complete-coverage claims or pass scored-outcome verification. All evidence already recorded is retained.
Replay constructs no provider, consumes the exact ordered request tape and checks
the committed decision record bodies byte for byte, including IDs, sequences and
hashes. The new run.json completion receipt also requires exact status, reason, tool-call count, spend and recording-count equality. A missing or truncated tape fails even if no decisions had been committed. A mismatch fails closed. The private manifest contains replay entropy for
record IDs and salted seals; planner tools and public exports cannot read it.
It is controller state, not an external timestamp or protection against an
operator who can read the public dataset.
Run one server per state directory; an OS lock enforces it. With uvicorn directly, set TRANSLUCENT_LEDGER_DIR and
TRANSLUCENT_STATE_DIR. Container instructions are in deploy/README.md.
| Variable | Effect |
|---|---|
TRANSLUCENT_LIVE=1 |
Allow paid runs (starting and resuming campaigns, paid planners). They also need an admin key, stored or TRANSLUCENT_ADMIN_TOKEN, and the profile's provider keys. |
TRANSLUCENT_ADMIN_TOKEN |
An admin key over every ledger, named TRANSLUCENT_ADMIN_NAME (default administrator). |
TRANSLUCENT_TEAM_TOKEN |
An engineer key over every ledger, named TRANSLUCENT_TEAM_NAME (default team). |
TRANSLUCENT_PROFILE |
Default model profile, dev or meta. |
TRANSLUCENT_CORS_ORIGINS |
Comma-separated browser origins. Default: none; a wildcard is rejected. |
TRANSLUCENT_API_DOCS=1 |
Serve /docs, /redoc and /openapi.json. Off by default. |
The two env tokens must be at least 32 characters; the server refuses to start with a shorter one. Use
translucent keys add for a generated key.
Every /api route needs a key except GET /api/session and GET /api/health. Reads are private too.
A key belongs to a credential: a name, a role and the ledgers it may use (* or a list of ids). Keys are random,
shown once, and stored only as their sha256, in credentials.json beside the ledgers (<data-root>/credentials.json,
or <state-dir>/credentials.json for a one-ledger server). The CLI works offline, and a running server sees changes
at once:
translucent keys add --name Ana --role engineer --data-root data
translucent keys add --name lab-bot --role planner --ledger lab --data-root data # --ledger is repeatable
translucent keys list --data-root data # never the keys themselves
translucent keys revoke KEY_ID --data-root data # ends its browser sessions tooA one-ledger server takes --state <state-dir> instead of --data-root. The first translucent start ... --serve
or translucent serve --data-root with no credentials.json (and no TRANSLUCENT_TEAM_TOKEN) makes one engineer
key, for --person or the OS user, prints it once and keeps it in <data-root>/team-key.
| Role | May |
|---|---|
viewer |
read the ledgers in its scope |
engineer |
read, and do a person's work: open a campaign with a free planner, approve, reject, revise, cancel, submit results, upload, import datasets, amend (lowering a budget, not raising one), note, conclude, and the people's actions below |
planner |
an agent's key: read, propose, define measurements, and cancel its own requests before approval |
admin |
an engineer who may also start paid runs (with TRANSLUCENT_LIVE=1 and the provider keys) and raise a budget by amendment |
Identity comes from the key. People's records are written under the key's name, and a planner key's under the
agent it names. Bodies may still carry author (a person) or agent (a planner), for older clients; when present
they must be the key's own name and kind, or the request is a 403 "This key acts as NAME".
HTTP clients send Authorization: Bearer <key>; keys are compared in constant time.
| Status | When |
|---|---|
401 Sign in |
no key, a wrong one, or a revoked one |
| 403 | the role may not do this ("A planner key cannot approve a request"), a viewer's write, a body naming someone else, or a browser write without the CSRF header |
404 Ledger not found |
a ledger outside the key's scope, the same answer as an unknown one |
| 409 | an approval of an older version (see the loop routes) |
| 429 | more than 10 wrong keys in 60 s from one address; a wrong key then gets 429 with Retry-After: 60, while a matching key always works |
The one-ledger /api/... routes count as ledger default for a key's scope. /api/budget needs any key.
Browser sessions. The web app asks for a key once and trades it for a cookie:
| Method and path | Body | Response |
|---|---|---|
POST /api/v1/session |
{"key": "..."} |
{name, role, kind, ledgers, expires_at} and an HttpOnly, SameSite=Strict cookie on /api, for 12 hours (Secure over https). A planner key is a 403: an agent sends its key with each request. |
GET /api/v1/session |
the same body for the cookie or bearer key, or 401 | |
POST /api/v1/session/end |
204; the session and the cookie are removed |
Sessions are stored hashed in the state folder, so a restart keeps them. Each is bound to the key that opened it:
revoking or replacing a stored key, or changing an env token, ends it, and an open event stream closes within
about 5 seconds. A write that rides the cookie must carry X-Translucent-Client: web (403 otherwise); bearer
writes need no such header. Event streams use the cookie (EventSource with credentials), so no key goes in a URL.
Every read below needs a key of any role that reads ledger default, except the first two.
| Endpoint | Returns |
|---|---|
GET /api/session |
Public. live, team and model: whether paid runs can start (live opt-in, an admin key and the default profile's provider keys), whether any key exists to sign in with, and the default planner's name. |
GET /api/health |
Public. {"ok": true}. |
GET /api/experiments |
Recorded experiments in the ledger. |
GET /api/campaigns |
Campaigns, each with a nullable job (queued, running, paused, finished, failed, interrupted). |
GET /api/campaigns/{id} |
One campaign and its job. |
GET /api/campaigns/{id}/events |
Server-sent events. Each event's ledger sequence is its SSE id and data.seq; send Last-Event-ID to receive only later events. The stream ends when the campaign does. |
GET /api/campaigns/{id}/bundle |
The replay bundle the web app renders (cached; refreshed on new records). |
GET /api/campaigns/{id}/people |
Questions with their status, notes, and people's sign-offs and challenges. |
GET /api/campaigns/{id}/graph |
The campaign's evidence graph: question, findings, reviews, measurements, runs, files and recording as nodes, with typed edges. Shapes in CONTRACTS.md. |
GET /api/findings/{claim_id}/evidence |
Everything a finding rests on, transitively: claim, measurements, runs, artifacts, reviews, problems (broken links), its current status, and signoffs: each person's review with evidence_digest (the finding and the evidence it was proposed on, with the runs invalidated by the time of the review), current_digest, current (true when nothing has changed since) and a plain text ("Ana reviewed this exact evidence", or "Evidence changed since Ana reviewed it (run ... invalidated)"). |
GET /api/compare?a=<id>&b=<id> |
What changed between two campaigns, two findings or two runs (ids of the same kind): structured changes plus plain sentences. For campaigns, findings are paired by test type and cited quantities. 400 for mixed kinds, 404 for an unknown id. |
GET /api/campaigns/{id}/next |
Deterministic advice computed from the ledger, never a verdict: advice (each rule, text, refs: a finding refused for a missing scale or clock, two attempts failing the same check, a supported finding resting on one method, a person's challenge nothing has answered yet, a loaded lesson nothing cites), and answer: the final answer's numbers with a unit (numbers) and the ones that match no supported finding's value, uncertainty or range (untraced). |
GET /api/campaigns/{id}/protocol |
The campaign's pre-registered metrics and method board: protocol (or null), pre_registered (the protocol provably precedes the start), and per metric its attempts (finding, method, value, unit, uncertainty, checker status, on_protocol with a protocol_note, replications) and decisions. |
GET /api/campaigns/{id}/forks |
What-if branches made from this campaign: each fork, its branch campaign, status (done, failed, needs_agent, or the agent job's status), reason, and the branch's findings with their status. |
GET /api/findings/{claim_id}/branches |
A finding's alternatives: chain (findings it replaces or that replace it), forks (what-if branches with their findings and status), same_quantity (findings in other campaigns that test the same quantity). |
GET /api/artifacts/{sha256-id} |
Recorded evidence bytes with their media type. Only existing ledger ids are served. Every artifact carries Content-Security-Policy: sandbox and nosniff; anything but plain data (CSV, TSV, text, JSON, Parquet), PNG/JPEG/GIF/WebP/TIFF images and video is sent as an attachment (HTML and SVG never render on the app's origin). |
GET /api/budget |
Local spend balances per provider. Never contacts a provider. |
POST /api/campaigns with {"objective": "...", "experiment_id": "reisecker2023/movie4", "profile": "meta", "budget": 2.0} returns 202 with the campaign and its queued job. The experiment must already be in the ledger;
paths are never taken from a request. Only one campaign runs at a time (409 otherwise). A budget must be a
finite number above zero and no more than the spending caps of the providers its profile calls ($25 for dev,
$170 for meta; 422 otherwise, as for resumes and queued what-ifs). A server without the profile's provider key
refuses the run with 503 before recording anything; a run that fails later is logged with its campaign id.
An optional "protocol" fixes the success metrics before the agent starts:
{"metrics": [{"id": "growth", "name": "Growth speed before the first short",
"quantities": ["growth_velocity", "derived:*"], "unit": "um/s",
"test": {"type": "value_within", "params": {"lo": 55, "hi": 72, "unit": "um/s"}},
"min_methods": 2, "needs_verification": true}]}The test is one of the checker's own predicate types with exactly its typed parameters (value_within: lo, hi,
unit; events_coincide: tol, optional unit; trend: direction, optional steady_tolerance); anything else,
free text included, is a 422 and no campaign is created. The protocol's author is the key's person; an author
field, if sent, must be that name. The protocol is recorded before the job is queued, and the
campaign's campaign.started event cites it. The agent then gives propose_finding a metric_id; a finding whose
test differs from the metric's (for example a window chosen after seeing the data) is still checked as usual but is
"off protocol" and can never win. The agent's declare_winner tool records a decision for a metric:
winnerwhen enough distinct methods (tool and analysis code) were attempted on protocol, the winner is on protocol and supported, a supported verification finding re-derived the winner's value through runs it shares with the winner only the experiment's own records, every replication of the winner succeeded, and no supported on-protocol attempt by another method disagrees beyond the combined uncertainty;tiewhen another method's attempt passes the same way and agrees;tradeoffwhen it passes but another metric of the protocol was won by a different method;insufficient_evidenceotherwise ("no winner yet"), with the checks that failed. It is recorded too.
A replication (translucent.rerun.replicate) re-executes every step behind a finding except the experiment's own
record lookups, as new runs with the same tools and arguments; SAM answers are replayed from their recording and
analysis code runs again in the sandbox. Every replication run is recorded under step replicate:<finding>:<n>,
failed ones included, and a failed one blocks a winner.
POST /api/campaigns/{id}/resume with {"budget": 2.0} (optional "profile") continues a stopped or interrupted
campaign from its evidence, with a fresh budget on top of what it has spent.
Jobs are stored as JSON under <state>/jobs/. A server restart marks running jobs as interrupted.
Every action is recorded under the key's person. A body needs no author; if it has one, it must be the key's name
(403 otherwise). Viewer and planner keys are refused with 403.
People's text (notes, answers, review reasons, what-if reasons), the free-text fields of an experiment manifest, and
agent-written analysis code have secret-looking strings (API keys, bearer tokens, private keys, password=...)
replaced by [secret removed] before anything is recorded; plain hex hashes and content addresses are kept. The
ledger is insert-only, so this happens at intake.
Notes, answers, reviews, control and what-ifs accept an Idempotency-Key header (1-128 letters, digits or _.:-).
The same key with the same body returns the first response without recording anything again; the same key with a
different body is a 409. Keys are kept per credential, method and path under <state>/idempotency/.
| Endpoint | Body |
|---|---|
POST /api/campaigns/{id}/notes |
{"text": "..."}. Reaches the agent before its next step. |
POST /api/campaigns/{id}/answers |
{"question_id": "...", "text": "..."}. Only open questions. |
POST /api/campaigns/{id}/reviews |
{"claim_id": "...", "verdict": "agree" | "challenge", "reason": "..."}. A challenge sends the agent back to re-examine. Neither changes the checker's verdict. |
POST /api/campaigns/{id}/forks |
A what-if: {"claim_id": "...", "reason": "...", "overrides": [{"quantity": "spatial_scale", "value": 164.4, "uncertainty": 1.6, "unit": "px/mm"}], "mode": "rerun"}. See below. |
POST /api/campaigns/{id}/control |
{"action": "pause" | "resume" | "stop"} |
PUT /api/uploads/{upload_id}/{filename}, then POST /api/experiments |
Add your own recording with its lab records; see BRING-YOUR-OWN.md. |
A person can ask what happens to a finding if one assumption behind it changes. The parent campaign is never
changed: the what-if is a new branch campaign plus a fork record, and both campaigns' event streams get a
fork.created annotation.
Overrides (each at most once per what-if): spatial_scale (px/mm or um/px), time_base_interval (seconds
between captured images), short_threshold_v (V), voltage_offset_s (s), and finding_window ([lo, hi] or a
tolerance, in the finding's unit). Values must be finite numbers, uncertainties at least 0, the scale, the time
between images and the short threshold greater than 0, a range's lo at most its hi, and units as listed (a range
in the finding's own unit). A what-if must change something the finding depends on. Anything else is a 422, and
nothing is recorded. A finding takes at most 20 what-ifs (429 after that).
"mode": "rerun"re-runs only the steps the override reaches, in their original order, with no model calls, then proposes the same finding in the branch for the checker. Everything else is cited from the parent unchanged. The changed value enters as a declared calibration ("declared by Ben in a what-if branch: ..."), so the branch's checks say what they are conditional on. If a step the override reaches needs a model or SAM, the branch stops withstatus: "needs_agent"and a plain reason; if a step no longer produces its result (e.g. a declared scale too loose to measure with),status: "failed".- A short rerun of local steps only (calibration lookups, voltage reads, growth measurement; at most 12 steps) needs an engineer or admin key and runs within the request (201 with the finished branch).
- A rerun that reaches agent-written analysis code (which runs in the remote sandbox), or a longer one, is a queued
job under the same rules as
POST /api/campaigns: an admin key, live opt-in and abudget(403 or 422 otherwise; 409 while another live campaign is queued, running or paused). The response is 201 withstatus: "queued"and the job (job.rerunis the fork id); the branch'scampaign.finishedevent carries the result.
"mode": "agent"(an admin key, live opt-in and abudget) records the declared values and queues the agent on the branch like any paid campaign.
The agent itself can never change an assumption: the tools' hidden assumption argument is accepted only from a
person's what-if, and only in that what-if's own branch. The MCP server offers list_branches (read-only).
The run_analysis_code tool takes Python code, inputs (measurement or artifact ids) and outputs
([{name, unit, label}]). Inputs appear under /work/inputs/ (inputs.json maps ids to files); the code writes
/work/outputs/<name>.json, either a number or {"value": 2.5, "uncertainty": {"value": 0.1, "kind": "ci95", "method": "fit"}}.
It runs in a fresh sandbox with networking blocked, no credentials and a 120 s lifetime. Limits: 1 MiB of code, 64 inputs, 32 MiB of input bytes, 32 scalar outputs. The code then runs a second time on the recorded inputs moved by a fraction of a percent; an output or uncertainty that doesn't move with them was typed in, and is refused.
create_app(data_root=D) (TRANSLUCENT_DATA_ROOT) serves every ledger under D/ledgers/<id>/, with the server's
state for each in D/state/<id>/ and one server per data root (D/state/server.lock). Ledger ids are 2-64
lowercase letters, digits, - and _. translucent.workspace.init_ledger(D, id, title=...) creates one; nothing
else does. An unknown id is a 404 and creates no folder. The /api/... routes above serve the ledger default
(the --ledger folder in single-ledger mode). A key works on the ledgers in its scope; any other is a 404.
In the Auth column, any is a key of any role, engineer is an engineer or admin key, and planner a planner key.
| Method and path | Auth | Body | Response |
|---|---|---|---|
GET /api/v1/ledgers |
any | [{id, title, campaigns, datasets}], only the ledgers in the key's scope |
|
GET /api/v1/ledgers/{id}/campaigns?cursor=&limit= |
any | {items: [CampaignSummary], next_cursor}: every kind (loop, video, intervention), newest first; limit 1-50 (50). A loop item's objective is the current version's one-line summary; a concluded one also carries assessment_headline and findings |
|
GET /api/v1/ledgers/{id}/campaigns/{cid}/events |
any | SSE, Last-Event-ID resumes after that seq; a loop campaign's stream ends at its conclusion |
|
GET /api/v1/ledgers/{id}/artifacts/{artifact-id} |
any | the bytes, served as GET /api/artifacts/... |
|
POST /api/v1/ledgers/{id}/video-campaigns |
admin, live | as POST /api/campaigns |
as POST /api/campaigns; one queued, running or paused job per ledger (409) |
PUT /api/v1/ledgers/{id}/uploads/{upload-id}/{name} |
engineer | raw bytes | {name, bytes}; at most 64 files per upload |
POST /api/v1/ledgers/{id}/datasets |
engineer | {manifest, upload_id} or {manifest, path} |
the DatasetRecord; 409 for a different dataset under a used id, 422 with the plain reason, 403 for a path the server may not read |
The manifest is always the JSON itself (import v2, or a v1 experiment manifest). path names a folder under the
server's import root (create_app(import_root=...), TRANSLUCENT_IMPORT_ROOT); without one, no path is read.
Absolute paths and .. outside the root, and links in the folder or on the way to it, are refused. Dataset ids are
per ledger: two ledgers can hold the same id. Writes accept Idempotency-Key as the /api/... ones do.
A loop campaign registers an objective on a system (GET .../systems), proposes experiments, has a person approve
each exact version, runs computational ones on the server and takes physical results from people, and concludes with
an outcome computed from the recorded comparisons. Every capability in translucent.loop_service.CAPABILITIES that a
person or an agent may call is one route below, named loop.<capability>; the same capabilities are MCP tools (see
AGENTS.md). The runner's and the comparator's capabilities are on no route.
The key names who acts (see Keys and roles): an engineer or admin key acts as its person, a
planner key as its agent. No body needs author or agent; either, if sent, must match the key. A planner key may
propose, define measurements and cancel its own requests before approval; it can never open or conclude a campaign,
approve, reject, revise, submit a result, amend or note.
Opening a campaign with a planner that spends nothing (external, scripted, domain_baseline) needs an engineer
key. A paid planner (muse...) needs an admin key, live opt-in, the profile's provider keys and room under the
providers' caps. GET /planners says whether each planner is ready for this key now, and why not. A planner that is
not ready is refused with that reason (403 for the role or live mode, 503 for a missing provider key or a spent cap);
the server never swaps in another planner or model. The objective's budget.model_usd must be a finite number of
dollars, 0 or more, and not above the caps of the providers its profile calls (dev $25, meta $170), as for a video
campaign; experiment_usd and max_wall_s must be finite and non-negative.
Method and path (under /api/v1/ledgers/{id}) |
Auth | Body | Response |
|---|---|---|---|
GET /systems |
any | [SystemSpec as JSON], with each system's objective_template |
|
GET /planners |
any | [{id, kind, label, paid, ready, reason}] for bo, external and muse; muse also has model (the exact model the server would call), profile, cap_usd, spent_usd, remaining_usd and default_budget_usd |
|
GET /datasets, GET /datasets/{did} |
any | summaries; one DatasetRecord (no raw values) |
|
POST /datasets |
engineer | see above | DatasetRecord |
POST /campaigns |
engineer (admin and live for a paid planner) | {title, objective, memory?} |
201 {campaign, view}; registered_by defaults to the key's person |
GET /campaigns/{cid}/view |
any | the campaign's LoopView |
|
GET /campaigns/{cid}/series?ref=&quantity=&max_points=&t0=&t1= |
any | {t, y, unit, t_unit, n, downsampled}; ref is a request or result id, or dataset/record |
|
POST /campaigns/{cid}/measurements |
engineer, planner | {name, spec} (ops/1) |
{record, measurements} |
POST /campaigns/{cid}/requests |
engineer, planner | {action, params, rationale, prediction, expected_effect, evidence_ids?, specimen?, due_by?} |
201 the request, with content_hash; a pending request with the same configuration is returned instead |
GET /campaigns/{cid}/requests/{rq} |
any | the request: versions, state, timeline, result | |
POST .../requests/{rq}/approve |
engineer | {content_hash, note?} |
the request; 409 when the hash is of an older version |
POST .../requests/{rq}/reject |
engineer | {content_hash, reason} |
the request |
POST .../requests/{rq}/revise |
engineer | {params, reason, approve?} |
the request at its next version |
POST .../requests/{rq}/cancel |
engineer, planner | {reason} |
the request (a planner: only its own, before approval) |
POST .../requests/{rq}/results |
engineer | the HumanUpload JSON (request_id, content_hash, executed_configuration, executed_units, specimen_id, table, time, scalars, notes) and upload_id; the uploader is the key's person |
the completed request; 422 {detail: {message, result_rejected: {record_id, reasons}}} when refused (the refusal is recorded) |
POST /campaigns/{cid}/amendments |
engineer (admin to raise a budget) | {reason, target?, constraints?, budget?} |
{amendment, comparisons} |
POST /campaigns/{cid}/notes |
engineer | {text} |
the human.note event |
POST /campaigns/{cid}/conclusion |
engineer | {text, unresolved_reasons?} |
the conclusion record; its outcome is computed, not sent |
GET /campaigns/{cid}/export |
any | <cid>.loop.json: the view and the receipt translucent verify re-checks |
An amendment's budget may set model_usd, experiments, experiment_usd and max_wall_s. An engineer may lower
them; raising one needs an admin key, and model_usd stays within the providers' caps.
A result's files are uploaded first with PUT .../uploads/{upload_id}/{name} and removed once the result is
accepted. Computational requests run on the server's loop runner thread (create_app(loop_workers=2); 0 runs none
here) as soon as they are approved; an MCP session's wait_for_request runs them too, and a claim lock with the
ledger's admission keeps each to one execution.
Statuses. 401: no key, or a wrong or revoked one (checked before a body is read). 403: the key's role may not do this, the body names someone other than the key, or the service refuses this actor (a planner cancelling another's request). 429: too many wrong keys from one address. 413: a body over its route's limit (1 MB of JSON; 16 MB for a dataset or experiment manifest; 500 MB for an uploaded file). 404: an unknown ledger or one outside the key's scope, an unknown campaign, request or dataset. 409: an approval of an older version, an Idempotency-Key reused for a different write, a dataset id clash, and any write to a concluded campaign. 422: anything else the ledger refuses, with the plain reason.
Idempotency-Key. Every write takes one. The key becomes part of the ids of the records the write appends (and of the campaign's id when opening), so a retry, including one after a restart that lost the API's state, finds the original records instead of writing twice; a different write under the same key is a 409. The API also keeps the first response for each key, as for every people's write, so a retry with the state kept gets that response byte for byte. A note (an event, not a record) and a result whose uploaded files were removed are recognised by the kept response only.
Several writers. The API server, any number of MCP sessions and the CLI may write one ledger at once. It must be on a local disk of one host: SQLite's write-ahead log needs shared memory, so network filesystems are not supported.
uv run --group dev translucent verify <ledger-dir | export-dir> # optional: --campaign <id>Replays every recorded checker review against the evidence available when it was made, and checks file hashes and
event order. When a campaign has a protocol, it also checks that the protocol was recorded before the campaign started
and re-decides every recorded decision on the ledger as it stood; the summary then ends with ", N decisions
re-checked" (it is unchanged for ledgers without decisions). Exit code 0 only when everything matches; otherwise one plain line per problem. An export
(translucent export <campaign-id> <out-dir> --ledger <ledger-dir>) carries what it needs to be verified with no
provider keys; keep its folder together.
Verification shows the evidence is consistent, not when it was recorded: a fully rewritten ledger with a matching manifest needs an external checkpoint to detect.
A stdio MCP server exposes list_campaigns, list_findings, get_evidence and verify over one ledger, the
loop-campaign reads, and with --write the measurement tools, propose_finding and the loop-campaign agent tools,
recorded as the agent named by --agent NAME (default local-agent); with --operator "<name>" also the person
tools. It needs no key: it acts as the local operator who started it. See AGENTS.md.
uv run translucent bench nist --out /path/to/output runs offline without provider keys. The default output directory remains evals; the command prints the destination and announces replacement of an existing INTERVENE.md. Markdown is written as UTF-8 with LF line endings on every platform. Optional --split loco|lofo|prequential and
--with-inferred|--without-inferred select a registered split or a refitted sensitivity. The default runs all
three splits with and without the five inferred heater-off trials, the 7-second heater-off lag sensitivity, and the 5:1 and 20:1 cost-ratio sensitivities. The registered default is 10-second lag with costs 100 unsafe, 10 unnecessary escalation and 0.05 per watched second. It writes INTERVENE.md, stamped JSON
results and local ledgers under the selected output directory. The report also includes explicitly post-hoc, ledger-recomputed cost-rule baseline variants, condition-clustered sign flips and an unsourced fixed-threshold sweep; these do not change the registered primary family. uv run translucent verify LEDGER rechecks the decisions,
seals, paid observations, updates, scores and gate claims. The self-contained scripted fixture verifies directly:
uv run translucent verify web/public/fixtures/nist-bench.json
uv run translucent export CAMPAIGN OUT --ledger LEDGERFor intervention campaigns the existing /api/campaigns/{id}/bundle and /graph endpoints work without a video.
These additional GET endpoints are read-only and make no model calls:
/api/campaigns/{id}/intervention: the optional v2 intervention projection, without its receipt./api/campaigns/{id}/intervention/{section}:trials,hypotheses,policies,decisions,scores,comparisons,updates,next_experiments,procedureorprotocol.
An absent campaign or section returns 404. POST is not supported. Revealed outcomes appear only after their recorded reveal; planner access goes through the narrower scoped tools below. The server is an operator's audit surface, not an outcome-hiding sandbox for a planner. Campaign comparisons include policy/split/subset score changes and observation costs. The evidence graph connects hypotheses, policies, decisions, real outcomes, artifacts and gate claims.
With translucent mcp --write --ledger LEDGER, use existing open_campaign with experiment: "nist:loco"
(or nist:lofo, nist:prequential), then list_tools and run_tool for that campaign. Its tool schema exposes
list_trials, get_trace, compute_features, record_hypothesis, propose_policy, evaluate_policy,
submit_policy, predict, commit_decision, observe, reveal, advance, score, propose_claim,
forecast_actions, next_experiments and write_procedure. advance requires the current held-out fold to be revealed. For LOCO/LOFO it creates fresh seals for the next condition/format and requires the new fold's training labels to be committed and revealed again. Submitted policy specs remain frozen. Prequential advancement registers the next Exp-number partition. The scripted planner completes every fold before its final performance claims. For LOCO/LOFO, propose the complete learning strategies before revealing any training label; subsequent fitting uses the current fold's revealed training data. Frozen comparators do not consume the agent-only submission cap K. predict returns separate world_model and decision_policy objects. Commit those executable outputs;
a changed probability or another policy’s observation is refused. Explicit abstentions are costed. forecast_actions records
a simulated two-node stop/watch/alternative-delay forecast from pre-off data, with no outcome access. observe requires a
prior watch commitment; post-off trace/feature requests name that policy and paid duration. No tool accepts a
filesystem path or arbitrary training ids. MCP ownership and --write rules remain in force. Finish with
write_procedure, which accepts supported claim ids and renders the declared model and decision rule with observed risk, decision cost and limits. Comparison claims use the registered paired mean-cost sign-flip test and Holm correction over submitted agent strategies times frozen baseline comparators (two × nine = 18 in the scripted campaign); unsafe McNemar remains descriptive. Trial corroboration labels appear in audit projections only after reveal.
InterventionCampaigner accepts ScriptedPlanner and exact RecordedPlanner replay for scripted
campaigns. For a metered planner use translucent intervene nist --profile meta --budget USD (default
120 tool calls per fold), or add --muse-only for the tool-free comparator. --dry-run estimates calls
and cost without contacting a provider. --replay RECORDINGS validates the complete request tape,
committed decision bytes and the recorded run status/counts/spend with no provider client. Each fold's
fresh planner context names the frozen registrations, submitted strategies and next steps. Frozen
comparators are registered automatically and committed by the controller before held-out reveal;
the planner cannot submit or change them. Model instruction rejections return tool errors; controller,
recording, provider rejection and token-length stops retain distinct statuses. Early scores are marked
partial. Legacy loose Muse summary files under data/recordings/intervention are refused because they
lack bound model-request receipts; an absent cache remains not_run. No recording or provider credential
is required by the benchmark. See INTERVENTION.md
for scientific limits and CONTRACTS.md for the optional bundle field.