CLI reference
tama model
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 [show] [--json]
printf '%s' "$KEY" | tama model set --origin https://api.openai.com --model gpt-5 [--json]
tama model set --none [--json]
tama model unset [--json]
tama model 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.
block-one-off-repairsrequires an affirmative semantic judgement. A no-model declaration does not authorize an unreviewed operation: this gate refuses withTAMA_HOOK_INFRASTRUCTURE: block-one-off-repairs: review unavailable: no model is declared on this machine …, including for an otherwise safe read or a command operating a dependency of the judge. Its structural recovery and build refusals still take precedence. The Model service choice does not turn this enforcement off; the gate's contract explains its decision, protected question-consumer operation and real CLI journey.- 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 (<what was read>). What was read names the registry the gate opened and what it found:<registry> holds no entry for the key <path>,<registry> does not exist, orwhether <registry> exists cannot be read: <cause>. A justification recorded under another key, or in a registry the gate does not read, is therefore seen in the refusal. 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 is asked the same typed questions, so a machine without Brama reads them too: each decision is one chat completion to <origin>/v1/chat/completions with the declared model, bound by response_format: json_schema (strict) to an object holding one {"choice": …} per question whose choice may only be one of that question's options. The reply's message is read as that document and checked the way Brama's is: decision protocol failure: the decision answered no `<key>`: … for a question left out, the declared model service <url> answered the decision with no message: … for a reply without one, and … with a message that is not the bound document (<cause>): … for one that does not parse; each refuses the hook with TAMA_HOOK_INFRASTRUCTURE: <hook>: review unavailable: <cause>. A judge that does not answer is never a pass: block-work-refusal, block-operator-commands and block-number-requests refuse the ending with the cause instead of letting it through (block-work-refusal gives it in full once and as one line while it stands). Only a machine that declared no model (set --none) has no reading of meaning: there block-work-refusal writes block-work-refusal: no model is declared on this machine, so whether the answer refuses work is not read; the open-work gates decide the stop, the other two write <hook>: no model is asked on this machine, the call passes: <cause>, each on stderr, and the answer goes through. The stop itself is decided by the gates that read Oko's registers and no model (block-stop-with-open-work, block-text-outside-tool-calls), so whether open work holds a turn never depends on Brama. 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 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 set` or remove it with `tama model 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 issued for that consumer, read from ~/.stado/<consumer>-skarbiec-token or from TAMA_BRAMA_TOKEN_FILE. Issue one with skarbiec grant issue <consumer> --capabilities call:brama#<route>,call:brama#best-decision-model --token-file <path> (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) holds the lifetime lock Stado keeps beside it (<state>.lock, held for as long as the resolver runs, recording its pid): a lock this reader can take means no resolver runs, and a held one counts only when the pid it records is the publisher's. A pid alone is not asked whether it runs: the system hands the number of an exited resolver to another process, and the file outlives the resolver that wrote it, so an adapter an exited process published is a port nobody holds. A state file that is absent, unreadable, naming no process, whose lock is absent, free or recorded by another pid, 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 a process that holds no lifetime lock, 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 local port 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. The Stado program is the one TAMA_STADO_CLI names, else stado on PATH, where Stado's installation links it; the Skarbiec program the verdict below asks is TAMA_SKARBIEC_CLI, else skarbiec on PATH. No program path is written into Tama, and a variable that cannot be read is refused naming it (TAMA_STADO_CLI cannot be read: …). The route journeys name their own Stado through TAMA_STADO_CLI, so a machine's installed Stado never answers them.
#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 set`, or return Tama to Brama with `tama model 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 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.