aorta chatFour sources, highest priority first:
--llm-provider, --llm-model, --no-redact.AORTA_CHAT_ plus the upper-cased name,
so chunk_size is AORTA_CHAT_CHUNK_SIZE. The prefix is not decoration: a
bare CHUNK_SIZE or ALLOWED_COMMANDS in a public tool that runs inside
other people’s job scripts is asking for a collision.$XDG_CONFIG_HOME/aorta/chat.toml, defaulting to
~/.config/aorta/chat.toml.The environment outranking the file is deliberate: a one-off export or a CI
job always wins over whatever is on disk.
Unknown keys in the profile are ignored at load time rather than rejected, so a
file written by a newer AORTA does not stop an older one from starting.
aorta chat config validate is where they are reported.
aorta chat config init --profile openai
--profile picks a starting point and then the wizard prompts for the few
fields that profile needs:
--profile |
For |
|---|---|
openai |
OpenAI itself |
openai-compatible |
Any OpenAI-wire endpoint: OpenRouter, Groq, Together, Fireworks, a self-hosted gateway |
azure-apim |
An Azure API Management gateway, which wants the key in a named header |
anthropic |
Native Anthropic protocol through LiteLLM (needs chat-all) |
local-vllm |
A vLLM server you run yourself |
--no-input writes the template without prompting, for scripting. --force
overwrites an existing file.
aorta chat config show # effective settings, credentials masked
aorta chat config show --json # same, machine-readable
aorta chat config validate # parses? no dead keys? not world-readable?
The profile holds your API key, and it is created mode 0600 — the mode is set
on the file descriptor before any bytes are written, so the key is never briefly
world-readable, and it is re-applied if a previous run or a careless editor left
the file at 0644.
Putting a credential at rest in a predictable path inside a tool whose day job is collecting diagnostic bundles obliges two guards, and both are in place:
aorta chat config show masks keys to their length and last four
characters. --reveal prints them in full. This exists because the likeliest
leak is not an attacker; it is a customer pasting their own config into a
support ticket.aorta bundle refuses to package the profile. The chat config path is
excluded explicitly, not by a filename convention.aorta chat config validate also fails a profile that holds a credential at a
permissive mode, which on a shared node is a real finding rather than a style
note.
If you would rather not store the key at all, leave it out of the file and
export AORTA_CHAT_REMOTE_LLM_API_KEY instead; the environment outranks the
file.
Chat does not persist a transcript by default. Set
AORTA_CHAT_SESSION_LOG=1 to append privacy-preserving decision events under
$XDG_STATE_HOME/aorta/chat/sessions/ (default
~/.local/state/aorta/chat/sessions/). The directory is mode 0700 and each
session JSONL file is mode 0600.
Both the browser and CLI record from the state returned by invoke_agent, so
they capture the same decisions without changing the CLI to a streaming path:
--no-redact
does not reach it: that flag governs what is sent to the model, not what
is written here;front_door, which entry point the turn came through (cli or ui), so a
browser demo and a scripted run are told apart rather than averaged;duration_seconds for the whole turn, iterations for how many critic
rounds the answer took, and whether the critic accepted the answer; andresolution: null attachment point keyed by (session_id, turn) for a
later verified outcome.Questions, selector rationale, plans, tool arguments, tool output, critic feedback, and answers are not stored in summary mode. Each becomes only character/byte/line/fence counts and a SHA-256 digest. Nothing reads these files back or sends them anywhere.
[!NOTE] One field is stored raw in summary mode, and it is a filesystem path. Any event that carries CIA job IDs also carries
jobs_root— the absolute path those IDs resolve under, verbatim in both modes. It is the resolvedjobs_pathsetting, which defaults to~/cia-jobsand is therefore under your home directory, username included.It is deliberately not summarised, because a digest of a path resolves nothing: the bundle, the autopsy report and the probe cells that say what a recorded job actually did all live under that root, and no other field in the record names it. A summary-mode log without it is a list of job IDs pointing at nowhere, which is what the empty
resolutionfield is waiting on.Nothing else escapes summarisation:
jobs_rootis a machine-level location, not a user-level one, so it does not vary with what was asked or which files were touched. If that is still more than you want to retain, pointjobs_pathat a shared location — which most clusters need anyway — or leave decision logging off.
AORTA_CHAT_SESSION_LOG=full stores those values, including selector rationale,
verbatim for an operator who explicitly needs a transcript. Full mode does not
scrub filesystem paths or IP addresses from the rationale. It emits one warning
per session naming the file and can contain source, paths, addresses,
credentials, and other model/tool output; protect and remove it accordingly.
Set the variable to 0 or leave it unset to disable all decision logging.
Every name below is a TOML key in the profile, and AORTA_CHAT_<NAME> in the
environment.
| Setting | Default | Meaning |
|---|---|---|
llm_provider |
vllm |
vllm (local server) / openai (any OpenAI-wire endpoint) / litellm (native Anthropic, Gemini, Bedrock). An unknown value raises, listing the accepted names. |
embedding_provider |
local |
local (a small model on CPU) / remote (an embeddings API). Independent of llm_provider, and every config init profile writes local — including the remote-chat ones, because the published index is built with the local model and cannot be read by any other. remote is a manual choice with consequences: see configuring a remote embedding provider. |
llm_tool_mode |
text |
text parses ACTION: tool(arg="v") lines out of the reply; native uses the provider’s function-calling API. Reasoning models need native — see providers. |
llm_provider = "vllm")| Setting | Default |
|---|---|
vllm_base_url |
http://localhost:8000/v1 |
vllm_model |
deepseek-ai/DeepSeek-Coder-V2-Lite-Instruct |
vllm_api_key |
EMPTY — vLLM ignores it, but the OpenAI client rejects an empty string |
llm_provider = "openai" or "litellm")| Setting | Default | Meaning |
|---|---|---|
remote_llm_model |
gpt-4o-mini |
The model id as the provider names it. For litellm, LiteLLM’s own id format. |
remote_llm_api_key |
(empty) | Required for openai; a missing value fails preflight rather than mid-query. Used by litellm too when set — only when it is empty does LiteLLM fall back to its own standard variables (ANTHROPIC_API_KEY, …). |
remote_llm_base_url |
(empty) | Empty means the provider default. Set it for anything else. |
remote_llm_auth_header |
(empty) | Header name for a gateway that does not take a bearer token. Honoured by both openai and litellm. |
remote_llm_extra_headers |
(empty) | Extra headers a gateway wants, as user=alice,x-tenant=acme or a JSON object. Honoured by both openai and litellm. Values are masked by config show and count as a credential for the 0600 check, because a gateway key put here is as sensitive as remote_llm_api_key. |
| Setting | Default | Meaning |
|---|---|---|
llm_max_tokens |
(unset) | Cap on generated tokens per call. Omit the key entirely rather than setting it empty. |
llm_timeout |
120 |
Seconds before one request is abandoned. |
llm_max_retries |
2 |
Transport-level retries per call. Multiplies spend on a flaky endpoint. |
| Setting | Default | Meaning |
|---|---|---|
embedding_model |
BAAI/bge-small-en-v1.5 |
Local model. Cannot be blank: an empty or whitespace-only value selects no model, so it is refused when the settings load. Remove the setting to take the default. |
model_cache_path |
$XDG_CACHE_HOME/aorta/chat/models |
Where the local model’s ONNX weights are cached. HF_HOME overrides it, which is what air-gapped pre-seeding uses. Explicit rather than fastembed’s own /tmp/fastembed_cache, which a reboot wipes and other users on a shared node can write. |
The five remote_embedding_* settings below are read by the embedding path only
when embedding_provider = "remote", which no profile selects. Setting them
without also setting embedding_provider changes nothing about how the index is
built or queried — though remote_embedding_api_key and
remote_embedding_extra_headers count as credentials for config validate’s
mode check either way. Setting them together with the selector is the procedure
below, and it obliges a
local index rebuild.
| Setting | Default | Meaning |
|---|---|---|
remote_embedding_model |
text-embedding-3-small |
Also decides the collection name, since dimensions differ per model. Cannot be blank, on the same rule as embedding_model. |
remote_embedding_api_key |
(empty) | Separate from the chat key, so the two can use different providers. Required: an empty value raises rather than falling back to the local model. |
remote_embedding_base_url |
(empty) | Empty means the provider default, which for an OpenAI-compatible client is api.openai.com. Set it for anything else — a gateway header with an empty base URL sends your corpus to OpenAI. |
remote_embedding_auth_header / remote_embedding_extra_headers |
(empty) | As on the chat side. Behind a gateway you normally set both or neither. |
| Setting | Default | Meaning |
|---|---|---|
aorta_path |
the installed aorta package |
The source tree retrieval and the file tools are scoped to. Only ever read. |
runs_path |
the working directory | Where your own sweep output directories live, for the run-artifact tools. |
index_path |
$XDG_CACHE_HOME/aorta/chat/index.sqlite |
The vector index, one file. |
repo_map_path |
$XDG_CACHE_HOME/aorta/chat/repo_map.md |
The generated function/class index. |
repo_map_prompt_max_chars |
20000 |
Cap on how much of the map is injected into the planner’s prompt; 0 disables the cap. The search_repo_map tool still queries the whole file. |
chunk_size / chunk_overlap |
512 / 50 |
Indexer text splitter. Changing either invalidates the index. |
Nothing writable defaults inside site-packages. An installed wheel is
read-only on a shared node, and a tool that writes into its own install
directory cannot be pip-upgraded cleanly.
| Setting | Default | Meaning |
|---|---|---|
retriever_k / retriever_fetch_k |
12 / 30 |
Chunks returned, and candidates fetched to select from. |
search_tool_k |
10 |
Results from the search_code tool. |
max_act_rounds / max_act_rounds_search |
5 / 8 |
Tool-loop budget for ordinary and search-shaped questions. The single biggest lever on cost. |
max_retry_iterations |
3 |
Critic retry budget. 0 disables the retry loop. |
| Setting | Default | Meaning |
|---|---|---|
enable_shell_tool |
false |
Register run_terminal_command. Off by default: it hands a model-authored string to a shell, so the agent is not given one unless you say so. While off, the tool is absent from the registry and from the prompts, not merely refused at call time. |
allowed_commands |
python,pytest,make,pip,grep,wc,head,tail,cat,ls,find |
Allowlist for run_terminal_command, applied per pipeline stage. Command chaining and redirection (;, &, backticks, $(...), >, <) are refused, since the allowlist checks executables. Accepts a,b,c or a JSON list. |
command_timeout |
60 |
Seconds before a run_terminal_command command is killed. |
redact |
true |
Rewrite filesystem paths and IP addresses out of outbound LLM requests. Does not cover the remote-embedding path. Read redaction before turning this off — and read it anyway for what it does not cover. |
These only apply where the cia extra is installed and the chat server can
reach a Slurm cluster. Without it the diagnostic tools are not registered and
none of this is read.
Each of these names something the chat tools and the agents both need to agree
on, so a single setting answers to two environment variables: the chat prefix,
and the name the agents use on their own. Setting either configures both halves
— the chat name wins if you set both. This is the one place AORTA_CHAT_* is
not the only spelling, and it is deliberate: CIA_JOBS_ROOT pointing one way
while the profile pointed another is the failure the shared name prevents.
| Setting | Also reads | Default | Meaning |
|---|---|---|---|
allow_cluster_jobs |
— | false |
Register the three tools that submit work: triage_kernel_source, triage_assembly_source, triage_workload. Off by default because they are outside the bound every other tool keeps — see extending. While off they are absent from the registry and the prompts, not refused at call time. Reading past jobs does not need it. |
jobs_path |
CIA_JOBS_ROOT |
(the agents’ own default, ~/cia-jobs) |
Where job records and bundles are written. Must be readable from every node that runs work, which on most clusters means a shared filesystem rather than /tmp. |
gpu_arch |
CIA_GPU_ARCH |
gfx950 |
The GPU the submitted work is built for. Used for the assembler target and passed to the agents as --arch, so both name the same chip. |
cia_demo_node |
CIA_DEMO_NODE |
(empty) | Pin work to one node. Empty lets the scheduler choose, which is correct everywhere except a demo. |
rocjitsu_build |
— | (empty) | The sanitizer backend. Unset means a sweep reports that it could not run, which is the honest outcome rather than reporting it found nothing. |
rocjitsu_preload |
— | (empty) | Preloaded into the sanitized process. ConSan’s hook is dlopened into one that has already loaded the host libstdc++, so without a newer one the tool library fails to load and the run reports a guardrail it never exercised. |
triage_timeout |
— | 1800 |
Seconds before one triage stops being waited for. The agents have their own internal timeouts; this is the backstop that keeps a wedged cluster job from hanging a chat turn. The abandoned run is asked to stop rather than left going. |
waitcheck_timeout |
— | 300 |
Seconds for one static assembly analysis, which needs no GPU and no queue. |
The scheduler knobs the agents read directly — CIA_PARTITION, CIA_TIME_LIMIT,
CIA_SSH_USER, CIA_SSH_HOST, CIA_SEARCH_ROOTS, CIA_CONTAINER_IMAGE,
CIA_SBATCH_EXTRA — have no chat setting. They describe the cluster rather than
the assistant, and are read from the environment the chat server runs in.
aorta chat ui is a Chainlit app, and Chainlit keeps its own configuration in
.chainlit/config.toml beside the app rather than in your profile. It writes
that file itself the first time it runs, with defaults chosen for a demo. Two
of them matter here, and both are committed set rather than left to be
regenerated.
| Setting | Shipped as | Why |
|---|---|---|
allow_origins |
["http://localhost:8000", "http://127.0.0.1:8000"] |
Chainlit’s default is ["*"]. The tools behind this UI submit cluster jobs, compile pasted HIP and — with enable_shell_tool — run commands, so a wildcard means any page a developer has open can talk to a local instance and start work on a GPU node. |
mask_user_env |
true |
Chainlit’s default renders API keys in the UI as plain text. The keys this server holds reach a model provider and a Slurm cluster. |
Serving anywhere other than localhost:8000 means editing allow_origins.
Those two are aorta chat ui’s own defaults, and they have to stay in step with
it: Chainlit reads allow_origins from the file and has no environment
override, so a port listed here that the command never serves on refuses the
browser of every default install.
aorta chat ui --host and --port, and the PORT variable in a launcher, all
move the socket without reaching into that file. The command compares the two at
startup and prints what to add, because the failure otherwise is a page that
loads and a websocket that never opens, with nothing on screen to say why.
The rest of the file is Chainlit’s own defaults. If you delete it, Chainlit
regenerates it — with allow_origins = ["*"] and mask_user_env = false — so
it is committed rather than ignored.
embedding_provider = "remote" is supported but selected by nothing: no
config init profile writes it, and there is no flag or prompt for it. It is a
deliberate, manual change, and this is the whole procedure.
One case, and it is narrow: a node that can reach an embeddings API, cannot reach Hugging Face, and cannot have its model cache pre-seeded. The local embedder downloads ~65 MB of ONNX weights from Hugging Face on first use, so a host firewalled off from it — but allowed out to a corporate gateway — has nothing to embed with until those weights arrive by some other route.
Copying them in is that other route, and it is the better one: the pre-seeded cache keeps embeddings local, free and unmetered on a host that cannot reach Hugging Face at all. Try it first. Remote embeddings are what is left when it is impractical — no second machine on the same AORTA version, no way to move 65 MB onto the node, or a policy against carrying model weights — and that is the only reason the code path exists.
Everything else that looks like a reason is not one:
Read these three before editing anything. Each of them is a consequence, not a risk to be managed.
aorta chat index fetch will not leave you with a usable index — it refuses
the published asset rather than installing it — and an already-fetched index
will refuse every query. You take over building the index yourself, on every
AORTA upgrade, until you go back.index runs, the floor is two
embedding calls per question, not one. Each search_code or
search_run_artifacts tool call adds another on top. The build
is the large number — the public corpus is roughly 5 MB of text over ~360
files, so on the order of one to two million tokens — but the per-query calls
are the ones that never stop. Check your provider’s own price list: at
text-embedding-3-small’s published rate the build is cents, and at a large
model’s, or through a gateway that adds a markup, it is not.redact = true rewrites paths and IP
addresses out of LLM requests. The embeddings request is a different
request and is sent verbatim. That matters most for the run-artifact
collection, which holds your own matrix.json and env.json and can carry
customer hostnames, filesystem layouts and environment variables — all of
which would be sent, unredacted, to the embeddings endpoint at
index runs time and never rewritten. redaction has the
full scope. If that corpus must not leave the machine, stop here.1. Set the selector and the five settings it turns on. Six in total, of which four are required and two apply only behind a gateway:
| Setting | ||
|---|---|---|
embedding_provider |
required | The selector. Nothing below reaches an embeddings API without it. |
remote_embedding_model |
required | Has a default, but set it explicitly — it names the collection. |
remote_embedding_base_url |
required in practice | Only omit it if you mean OpenAI’s own API. |
remote_embedding_api_key |
required | Empty raises at first use. |
remote_embedding_auth_header |
gateway only | Header name instead of a bearer token. For Azure API Management that is exactly Ocp-Apim-Subscription-Key. |
remote_embedding_extra_headers |
gateway only | Any other headers the gateway wants. |
Coming from the azure-apim profile? That profile pre-fills
remote_llm_auth_header = "Ocp-Apim-Subscription-Key" for the chat side, and it
used to pre-fill remote_embedding_auth_header with the same string. It no
longer does — embeddings are local in that profile now, so there was nothing for
it to configure — which makes this the one value the wizard used to hand you and
no longer does. It is the same string on both sides.
In ~/.config/aorta/chat.toml:
# The selector. Without this nothing below reaches an embeddings API.
embedding_provider = "remote"
# The model. Also decides the collection name, because dimensions differ per
# model and two models' vectors cannot share a table.
remote_embedding_model = "text-embedding-3-small"
# The endpoint. Do not leave this empty unless you really mean OpenAI's own
# API: empty resolves to api.openai.com, so an empty base URL plus a gateway
# auth header sends your corpus to OpenAI with a header it does not read.
remote_embedding_base_url = "https://gateway.example.com/openai/v1"
# The key. Separate from remote_llm_api_key, so chat and embeddings can use
# different providers. Empty raises at first use rather than silently falling
# back to the local model.
remote_embedding_api_key = "..."
# The last two only behind a gateway that does not take a bearer token. Same
# names and same meaning as the remote_llm_* pair; omit both against a provider
# that takes an Authorization header, which is most of them.
remote_embedding_auth_header = "Ocp-Apim-Subscription-Key"
remote_embedding_extra_headers = { user = "alice", x-tenant = "acme" }
Or in the environment, which outranks the file:
export AORTA_CHAT_EMBEDDING_PROVIDER=remote
export AORTA_CHAT_REMOTE_EMBEDDING_MODEL=text-embedding-3-small
export AORTA_CHAT_REMOTE_EMBEDDING_BASE_URL=https://gateway.example.com/openai/v1
export AORTA_CHAT_REMOTE_EMBEDDING_API_KEY=...
export AORTA_CHAT_REMOTE_EMBEDDING_AUTH_HEADER=Ocp-Apim-Subscription-Key
export AORTA_CHAT_REMOTE_EMBEDDING_EXTRA_HEADERS=user=alice,x-tenant=acme
remote_embedding_api_key and the values in remote_embedding_extra_headers
are both treated as credentials: masked by config show, and enough on their
own to make config validate fail a profile that is not 0600.
2. Confirm what it resolved to, before spending anything.
aorta chat config show
Check the endpoint is the one you meant. An empty or mistyped
remote_embedding_base_url is the failure worth catching here, because the
symptom is a 401 from a third party you did not choose rather than an error
about your configuration.
3. Rebuild the index. This step is mandatory, not a refresh.
aorta chat index build # the source collection
aorta chat index runs # the run-artifact collection, if you use it
index runs is the one to think twice about: it is the command that sends your
own matrix.json and env.json to the embeddings endpoint, and it re-sends
them every time you rebuild. Skipping it leaves the run-artifact tools without
an index they can read, which is a worse assistant but not an egress you did
not choose.
Not index fetch — it refuses the published asset rather than installing it,
correctly, because that asset is the local model’s. index build
embeds everything under aorta_path through the provider you just configured,
and writes it to a collection named after that provider and model, so the local
collection already in the file is not overwritten.
Know what you are giving up in coverage as well as in cost: aorta_path
defaults to the installed aorta package, so a local build indexes the code
but not docs/ or README.md, which the published asset does carry. Point
aorta_path at a source checkout if you want the prose too. See
the RAG index.
4. Verify.
aorta chat doctor
The index check should pass, and the embedding provider line should name your
model and the endpoint you configured.
provider default where you expected an endpoint is not in itself a
failure: that is what the line prints whenever remote_embedding_base_url is
empty, which is the supported way to mean OpenAI’s own API. It is a symptom only
if you meant to point at a gateway — in which case step 1 did not take effect,
and your corpus is going to OpenAI with a header it does not read.
A refusal here means the index and the configuration still disagree — usually step 3 was skipped, or was run before step 1 took effect.
Remove or unset the six settings and re-fetch:
aorta chat index fetch
The index this replaces is the one step 3 built locally, and that is the point: going back means giving up the local build for the published asset. If the command declines for that reason — a downloaded asset can always be downloaded again, where a local build may not be reproducible — the refusal names the flag that overrides it, and overriding is the right answer here.
That is the whole way back only if embedding_model is still at its default.
It is not one of the six, and it is the one other setting the published asset is
checked against: manifest.validate refuses on the model name, on the collection
name and on the embedding identity, and for the local provider all three are
derived from embedding_model. A hand-set value is therefore refused exactly as
a remote provider was. Clear it as well, or keep it and rebuild with
aorta chat index build.
chunk_size and chunk_overlap are a different case and do not block the
re-fetch. They are compared, but as warnings: the vectors are still the published
model’s, and the chunker only ran at build time, so what a mismatch costs you is
that retrieved spans are not the size the prompt budget was tuned for. Restore
them if you changed them, but they will not stop index fetch and they never
made the published asset unusable.
The two providers use different collection names and coexist in the one
.sqlite file, so switching to remote never disturbed the local collection:
the remote index build staged a database that lacked it, and installing a
staged database carries over whatever collections the incoming one is missing.
Coming back is not symmetric, because the published asset does contain a local
collection. On the default embedding_model it has the same name as yours, so
index fetch installs the published one over it, and restores the manifest that
the remote index build had rewritten to name the remote model. That is what
you want when your local collection came from the published asset in the first
place. When you built it yourself over a wider corpus — an aorta_path pointing
at a source checkout, so docs/ and README.md were indexed too — reach for
aorta chat index build instead: it restores the manifest just as well and
keeps your own corpus rather than replacing it with the package-only build.
# ~/.config/aorta/chat.toml
llm_provider = "openai"
remote_llm_model = "gpt-4o-mini"
remote_llm_api_key = "sk-..."
llm_tool_mode = "native"
embedding_provider = "local"
aorta_path = "/home/me/src/aorta"
runs_path = "/home/me/sweeps"
max_act_rounds_search = 4