CLI reference
tama model-provider
Chooses the model service Tama's model calls ask — the objective reviewer, the stop review, frustration drafting and adaptive disputes — and checks whether it accepts this machine. Brama is the default; a machine without Brama declares any OpenAI-compatible chat-completions service instead, or declares that it has no model at all.
#Invocation
tama model-provider [show] [--json]
printf '%s' "$KEY" | tama model-provider set --origin https://api.openai.com --model gpt-5 [--json]
tama model-provider set --none [--json]
tama model-provider unset [--json]
tama model-provider check [--json]
tama doctor [--json]
A machine with no model
Tama does not need Brama, or any model, to hold a session to its rules. set --none writes {"provider": "none"} into the same declaration file, and show then prints provider: none, model: (not declared), declaredIn and judgedBy. From then on:
- Every deterministic hook enforces exactly as before.
- A stop gate that would ask a model whether the ending answer breaks its rule reads the session's recorded attestation instead: the task's own JSON statement that it is finished, recorded with
tama attestation record --file FILE --session ID. With no attestation recorded the gate refuses and names the command that records one; with one that falls short it refuses with each field's reason. The turn-end gatescheck-stop-asking,block-false-blockersandblock-stop-on-own-workask no model on any machine: they read the claims the ending answer carries (tama attestation standard --turn). - A hook that only asks a model a question about a text (whether it claims a failure, which tests are smoke tests) finds nothing and writes
<hook>: no model is declared on this machine, so this question is not readto its log. - The write gates that would ask a model about a file read the file's recorded justification instead (
tama justify record --kind file --file PATH --justification TEXT):check-unsubstantiated-constantsadmits the numbers of a justified file and refuses each one of a file with none, andblock-one-off-commandsadmits the commands a justified file declares and refuses a file with none, naming each command, withno model is declared on this machine to judge whether each is a general primitive, and no justification is recorded for this file. No judge request is kept andtama commands judgeis not named. review-pushasks no model on any machine, whatever is declared: every commit the push adds carries the commit attestation in its own message, and a commit that lacks a claim is refused naming it (tama attestation standard --commitprints the block). The formatter andstado quality checkthe dispatcher runs before it still refuse a push.checkexits 1 withno model is declared on this machine, so there is no service to check.
unset returns Tama to Brama. Tama Desktop's Model service section sends the same operations: tama request model-provider (the choice and its check; with no model the check says there is no service instead of probing Brama), tama request model-provider/set with {"none": true}, and tama request model-provider/unset. A service and its key are declared only in the terminal, so the key never crosses a request; model-provider/set with any other body is refused with that sentence.
Typed questions
A question with a fixed set of answers is not asked as chat. Whether a text says something (check-failure-inspection-claim, pre-edit-trajectory-evidence, block-unsearched-blocker-claim, the smoke-test gate), which candidates a text names (which test files are smoke tests), whether a prompt is frustration with the assistant (the frustration detector) and whether a prompt disputes a block (the adaptive bridge) are each sent to Brama's POST /v1/decisions as a choice between yes and no, one per candidate, and the hook reads the side Brama chose. No answer word is negotiated in a prompt and no reply is parsed. The alias follows the route: best (the default) asks best-decision-model, decision-model and best-decision-model are taken as written, and any other TAMA_OBJECTIVE_AUTHORITY_MODEL_ROUTE is refused with route `<route>` cannot answer a typed question: Brama's POST /v1/decisions answers only `decision-model` and `best-decision-model` (`best` asks the latter); set TAMA_OBJECTIVE_AUTHORITY_MODEL_ROUTE to one of them. The token must carry call:brama#best-decision-model (or call:brama#decision-model) beside the chat route; without it Brama answers 403 naming the routes the credential is scoped to, and the hook refuses with TAMA_HOOK_INFRASTRUCTURE: <hook>: review unavailable: Brama rejected the request ….
A service declared with set --origin answers chat completions only, so it is never sent a typed question. A hook that asks whether a text says something or names a candidate finds nothing there and writes <hook>: the model service declared in <file> (<origin>) answers chat completions only, and a typed question is answered by Brama's POST /v1/decisions; `tama model-provider unset` returns this machine to Brama, so this question is not read; the frustration detector and the adaptive bridge record the same sentence as their failure. The stop review stays on chat on every service. The gates that choose among their own options with the judge's reason (model_judgement::choose) ask through Oko, oko gateway object: a chat completion bound to a strict JSON Schema, answered by Brama or by the OpenAI-compatible service oko model-service set declared, because the refusal quotes the reason the model gave and no declared option can carry it.
#Output and state
set reads the service's API key from standard input only and writes the origin, model and key owner-only (mode 0600) to ~/.tama/model-provider.json; TAMA_MODEL_PROVIDER_PATH names another file. From then on every Tama model call posts to <origin>/v1/chat/completions with that key as its bearer and asks for that model. unset removes the file and Tama asks Brama again. Every verb prints the choice that stands afterwards: provider (brama or openai-compatible), model, and for a declared service origin and declaredIn, as key: value lines or as JSON with --json. The key is never printed.
#Refusals and failures
set needs --origin URL and --model MODEL (the API key is read from standard input), or --none for a machine with no model— exit 2.--origin must be HTTPS, or HTTP on loopback, with no credentials, query or fragment, and --model must name a model: got "…" and "…"— exit 2.the API key is read from standard input, and standard input was empty— exit 2.no model provider is declared in <file>; Tama already asks Brama—unsetwith nothing declared, exit 1.<file> names no complete model provider (origin, model and key); declare it again with `tama model-provider set` or remove it with `tama model-provider unset`— a damaged declaration; every model call refuses with the same sentence until it is repaired.- With nothing declared,
showexits 1 with the route resolver's own sentence when Brama's route cannot be resolved, instead of printing an emptymodel. the model provider declaration <file> cannot be read: <cause>or<file> is not a model provider declaration: <cause>— every verb, and every hook that reads the choice, refuses with the cause.takes show, set, unset or check; got <verb>— exit 2.
check
Presents the credential of the service in force to its bearer-only GET /v1/models and reports what it answered. That request is not billable and calls no provider. The bearer reaches curl through a --config document on standard input, never through its arguments. Tama Desktop shows the same read as the Model service section of the Install screen.
tama doctor [--json] runs this same diagnostic without changing the provider. It names discovery and credential failures, refuses a declared none provider, and requires catalogue acceptance for success. It does not send a judge request: a successful catalogue check is not a model verdict. With --json, failures before a report exists are emitted as {"error":"<actual cause>"} with an unsuccessful exit. Unknown arguments are refused as doctor: unexpected argument "<argument>"; use tama doctor [--json]. The corresponding graphical diagnostic remains Install → Model service.
#Identity and credential
For Brama, Tama's identity at the gateway is a Skarbiec consumer, tama by default and TAMA_BRAMA_CONSUMER when set. Its credential is a token minted for that consumer, read from ~/.stado/<consumer>-skarbiec-token or from TAMA_BRAMA_TOKEN_FILE. Mint one with skarbiec token-mint <consumer> --capabilities call:brama#<route>,call:brama#best-decision-model (the second grant is the alias typed questions ask); the grants inside the token decide which routes that consumer may ask for, and Brama learns the client and its routes by introspecting the token at Skarbiec. For a declared service the credential is the key set stored, and no Skarbiec grant is asked.
Brama's address is resolved exactly as clean and the objective reviewer resolve it: TAMA_OBJECTIVE_AUTHORITY_MODEL_ROUTER_URL, STADO_MODEL_ROUTER_URL or BRAMA_URL when one is set; otherwise the fleet route obtained with stado service directory connect brama --consumer <adapter> --json. The adapter is tama by default and TAMA_BRAMA_ADAPTER_CONSUMER when set. There is no public-hostname fallback. A declared service's address is its origin.
Before any Stado child is started, the adapter this machine's resolver publishes for Tama's consumer is read from the state stado serve --resolver writes for programs on its host (~/.stado/resolver-state.json, or STADO_RESOLVER_STATE_FILE when Stado itself is told to publish elsewhere). It is read in the shape Stado writes it: listening belongs to the process and is true while it holds its listeners bound, and an adapter it does not serve carries an unserved reason; no adapter carries a listening field of its own. While the process declares listening: true, an adapter whose service is brama, whose consumer is the adapter name and which carries no unserved reason names the route as http://<bind>. It is what stado service directory connect would answer, read without starting a process, so a judge does not depend on a Stado child answering. The state is trusted only while the process that published it (pid) still runs, as ps -p answers it, the same rule Stado applies to its own published state: the file outlives the resolver that wrote it, and an adapter a process that has exited published is a port nobody holds. A state file that is absent, unreadable, naming no process, published by a process that is gone, whose process does not declare listening: true, or listing no served adapter for the consumer is said so on stderr and the search goes on to Stado. The journey under tests/brama-route/ writes the state in Stado's shape: it publishes a loopback listener as the adapter and reads the chat request arriving there, then publishes the same adapter with an unserved reason, and then as published by an exited process, and reads the search naming the absent Stado both times.
Every request reads the current resolver publication or asks Stado to resolve the fleet route. Tama does not persist a bare URL: such a file cannot establish the resolver's lifetime, directory generation or consumer admission. Obsolete ~/.tama/brama-route/<adapter>.url files are ignored, including unreadable ones. Stado spawn, exit, JSON and URL failures retain the actual operation and cause. Discovery has no shared URL-cache lock, so one caller does not hold other callers behind its discovery process.
Discovery does not detach a child or discard its error output. In a native hook the Stado child inherits the hook's owned lifetime group. Cold discovery can wait on Stado; this is not a millisecond-response guarantee. Stop-review scheduling belongs to the harness adapter, which must keep that work outside its bounded callback.
A fleet route this machine cannot hand out or verify — its resolver holds no listener, or declares no adapter for the consumer — does not end the search: Brama is looked for across the fleet through a relay this process owns, stado resolver relay brama --consumer <adapter> --bind <loopback address>:<a port stado host free-port-local handed out> --json. Stado reads the directory from the authority, resolves Brama for the consumer and carries each connection over its own SSH session to Brama's host; the relay announces its loopback URL on its first line and lives while the caller holds its standard input, so it ends with the credentials that served one request and its URL is never remembered. When the relay fails too, the refusal carries both causes: the discovery failure and the relay's own. Every judge on a workstation whose resolver stopped therefore still reaches Brama as long as Brama answers on its host and the fleet channel to that host is open.
The route and the relay are both answered by this machine's Stado, and Tama reaches no host past them: it opens no SSH session of its own and reads no copy of the fleet registry. When stado service directory connect cannot run or refuses and the relay fails too, the model call is refused naming both causes, so the Stado failure is what the refusal shows.
#Output
Text output is ten lines: the provider (brama or the declared origin), the chat endpoint the client would post to, the transport verdict, the probe address with the HTTP status it returned, detail — the number of models offered on success, or the service's own error body — then consumer, token file and route, then what this machine's authority answers about that route on that bearer (for a declared service, not asked), and finally one verdict. --json emits the same fields as one object, including local_grant and verdict. Inspection does not change provider selection or credentials; resolving a cold Brama address can populate the shared route cache.
#Reading the verdict
For Brama the verdict line exists because 401 stands for three different states at once: a bearer can be valid at its own authority while the gateway is deployed without the variables that let it ask, and a bare status cannot tell the two apart. The sentences are:
- the model catalogue accepts this bearer; chat was not tested — the catalogue answered
200. A chat request may still fail at a different proxy, on its route, or at its local capability check. Inspect the actualfrustration-drafteroutcome intama rules digest --jsonbefore claiming that automatic drafts work. - this machine's authority grants the route on this bearer, and the gateway still refuses it — the gateway is not asking this authority. Check that its deployment carries
WC_SKARBIEC_URL,BRAMA_SKARBIEC_CONSUMERandBRAMA_SKARBIEC_TOKEN_FILE. - this machine's authority does not grant this route to this consumer — issue it with
skarbiec grant issue <consumer> --capabilities call:brama#<route> --token-file <path>. - the gateway refuses this bearer and this machine's authority could not be asked — the local vault or its CLI is the first thing to repair.
For a declared service: <origin>'s model catalogue accepts the declared key; chat was not tested, or <origin> refuses the declared key with <status>: declare the service again with `tama model-provider set`, or return Tama to Brama with `tama model-provider unset`. Either service: nothing answered at that address when no HTTP answer arrived.
#Refusals
check exits 1 unless the service answered 200 through an allowed transport. A missing Brama token exits 1 with no Brama token for consumer <consumer> at <path>: <cause>; mint one with …; an empty file is refused as is empty and a file holding more than one word as does not hold one token. An address off loopback that is not HTTPS, or one carrying credentials, a query or a fragment, is refused by the client before anything is sent. A damaged declaration is refused with the sentence above.
If discovery or credential reading fails before a catalogue report exists, check --json emits {"error":"<actual cause>"} on stdout and exits unsuccessfully. The same cause remains on stderr and in Desktop's check.error; a missing route is not an empty successful report.
An existing token path that cannot be read instead reports
cannot read Brama token for consumer <consumer> at <path>: <cause>.
Permission, directory and decoding errors preserve their actual cause; they
do not claim the grant is absent or advise replacing it. The real catalogue
journey checks a directory-as-token refusal against the observed filesystem
error before any network request.
#Visible child waits in provider code
block-silent-waits checks added Rust waits through tama hook block-silent-waits, reading the write or edit payload on stdin. A child reap inside the shared stado_wait::blocking API is a visible wait, including a multiline closure, just as a wait inside stado::wait or crate::wait is. A sibling bare child.wait() outside that call remains refused. The decision names the file, operation and offending line; installed Session/Posture surfaces consume that same decision.
The existing silent_waits_refusal journey retains the declared clean candidate, built executable digest, payloads, commands, statuses and decisions. It covers a shared-crate multiline reap, a bare sibling and an unrelated namespace ending in the same spelling.
Chat and catalogue requests validate the transport policy and resolver channel before starting the HTTP client. They disable implicit curl startup configuration, so a user curl configuration cannot silently add retries or redirect handling to the declared request. Catalogue response collection prints the shared visible wait just like chat collection.
If writing a request into the HTTP client's input pipe fails, Tama terminates its owned client and visibly collects its output before returning. The refusal preserves the original input error together with the observed client exit and stderr, or the concrete termination/collection error. Cleanup failure never replaces the primary request failure or becomes a successful empty answer.
The endpoint and headers reach the HTTP client as a config document on its
standard input, so the bearer never enters an argument vector; the chat body
reaches it as a file under ~/.tama/judge-requests/ that curl reads
by path, written by the hook process before the client starts and removed when
its request ends. curl reads a config line only up to its own fixed length, and
a stop hook's prompt carries the turn's transcript: as one config line it
outgrew that length and curl answered cannot read config from -
(exit 26) with nothing naming the line. A config document curl still refuses is
now named by its lines: Brama request failed (exit status: 26): curl:
cannot read config from -; …; the config document curl read from standard
input had the lines url (N bytes), request (N bytes), header (N bytes), header
(N bytes), header (N bytes), data-binary (N bytes), silent (6 bytes),
show-error (10 bytes), write-out (N bytes); the request body was
<path> (N bytes); the values themselves, the bearer among them,
are never printed. Any other client failure names the body file and its size
beside curl's own stderr. The real journey tests/judge-request/body.rs
sends a prompt longer than a config line to the real Brama endpoint
(TAMA_TEST_BRAMA_ENDPOINT, TAMA_TEST_BRAMA_TOKEN_FILE,
TAMA_TEST_OVERSIZED_BYTES) and requires it carried, the body file
gone, and a bearer of that length refused with the lines named.
#Route qualification
The registered model_provider_route journey under tests/model-provider/route.rs invokes the actual built CLI and retains candidate revision, binary digest, command, exit status and output. It checks that an obsolete URL file cannot hide an absent Stado executable. TAMA_TEST_SOURCE_REVISION must identify the clean candidate. Its explicit real-discovery case requires TAMA_TEST_BRAMA_HOME: an isolated, configured home with a real installed Stado and real Brama credentials. It refuses the operator's home, retains the Stado digest and catalogue report, requires the real catalogue to accept the credential and checks that discovery creates no URL cache.
The shared doctor runner clears inherited environment variables, then supplies only the tool search path, the selected isolated home and a temporary directory inside the retained run area. Resolver-state, model-provider and credential-file overrides from the invoking session cannot redirect those checks to the operator's services.
The same journey includes the real CLI refusal for a plaintext remote catalogue endpoint. It requires the transport-policy cause before the HTTP response wait and checks that the fixture credential never enters output. This negative case performs no model call.
The owned-runtime cancellation journey also dispatches the actual
model-provider check --json command against a TCP listener that
accepts the catalogue request but sends no response. Cancellation must close
that real connection, report a cancelled dispatch and leave no running hook.
Candidate receipts name the dispatched command as well as the runtime binary;
the journey sends no invented provider response.
Return to the complete CLI command tree.