How termite is built
termite is a safety tool, so readers must be able to check it. This page
describes the design. tests/architecture.rs fails the build when the code
does not match it, so a change to the design must update this page in the same commit.
Rules
- Small files. No file has more than 400 lines of code, not counting tests. A file that grows is split along a boundary a reader would name.
- One direction. A module uses only the modules listed for it below. There are no cycles.
- Side effects stay in the binary. The library prints nothing, starts no process
and spawns no thread. Only
generateanddecidetalk to a server. Thetmitebinary owns the terminal, processes and threads. - Every input is bounded. Anything read from a model, a server or a repository has a limit, listed under Memory.
- Errors never loosen a check. Doubt, an error or a timeout makes the answer stricter.
- Model text is data. It is never run by termite and never written to a
terminal without
to_printable.
Modules
Each row may use only the rows above it.
| Module | Job | Contract | Uses |
|---|---|---|---|
candidate |
a command, its description and risk label | plain data | |
command |
checks on any command a model wrote | pure; check_command and to_printable read only their argument |
|
context |
the OS, shell, folder and session | plain data | |
error |
every way an answer can fail | typed; says which service failed | |
home |
termite’s private folder and script names | refuses folders that others can write to; writes files 0600 | |
provenance |
the log of what termite installed | hashes only; termite never adds to a log that fails verification | home |
progress |
the current stage of an answer | carries only termite’s own words and numbers; cannot change the answer | |
heartbeat |
checks the server during a long wait | reports only; the time limit decides when to give up | progress |
query |
reads ?, ??, ?x |
pure | |
sse |
reads a streamed reply | any split of the bytes gives the same reply; bounded | error |
merge |
joins commands that do the same thing | pure; the most harmful risk label wins | candidate |
policy |
what ?x may inject with no question |
an allowlist; anything unknown is refused; reads only git config | command |
script |
reads a script or a stream and rates each command | reads text only; memory stays bounded; anything it cannot read is risky | command, hazard, policy, rating |
config |
the config file | checks URLs; holds no token | error, home |
decide |
the decision model’s client | can only make an answer stricter than the thresholds allow | candidate, command, config, context, error |
generate |
the chat model’s client | 1 to 5 candidates, or a typed error, which includes the model declining | candidate, command, config, context, error, heartbeat, progress, sse |
draft |
writes a script as a draft | saves nothing itself and runs nothing; a critical hazard is withheld with no second try | command, context, decide, error, generate, hazard, home, progress, rating, script |
answer |
the flow: generate, check, merge, rank, gate | never fails open; local checks run before the network | all of the above except config, heartbeat, home, sse |
Inside a module, policy is split into shell (reading a command line),
programs (the allowlist), options (shared option checks), services
(git, docker, kubectl, package tools), secrets (sensitive paths) and
git_config (a repository’s own config).
The binary adds check (tmite check), scripts (tmite script and
tmite scripts), script_tools (the only code that calls bash -n and
shellcheck, which parse and never run), library (installs the bash library),
provenance (tmite provenance, tmite alerts) and provenance_key (the only
code that talks to the system key store).
The binary has main (arguments and wiring), output (the three output
formats), progress (the progress line) and setup (discover, installed,
prompt, fix, hub).
Flow
request ─▶ query ─▶ generate ─▶ command checks ─▶ merge ─▶ decide.rank
│
? ──▶ Confirm ◀───────────┤
?? ──▶ Choose ◀───────────┤
?x ──▶ policy ─▶ decide.safety ─▶ StraightIn
Confirm
Refused
tmite only prints. The widgets in shell/, one each for zsh, bash and fish, place
the command at the prompt. tmite shell prints them, so nothing has to be copied
into place.
scripts/team.sh is the only route the agent tools use. place.rs says
what separates this process from the machine, and the eval tool’s gate.rs decides
whether a model test may start. Keep those two checks in code, never in a prompt.
Memory
| Input | Limit | Where |
|---|---|---|
| A model’s reply | 256 KiB | sse::MAX_REPLY_BYTES |
| One line of a stream | 64 KiB | sse |
| Candidates | 5 | generate::MAX_CANDIDATES |
| One command | MAX_COMMAND_CHARS |
command |
| A git config file | 1 MiB; larger counts as unsafe | policy::git_config |
| Any request | the configured time limit | generate, decide |
A stream keeps only the reply text and one partial line. The progress thread keeps the current stage, and ends before the answer is printed.
Checks
| Check | Proves |
|---|---|
cargo test |
behaviour, including every refusal table |
tests/architecture.rs |
the rules on this page |
tests/widget.rs |
the real zsh, bash and fish widgets in a pseudo-terminal |
tests/cli.rs |
the real binary against a hostile fake server |
src/adversarial_tests.rs |
hostile commands in every disguise, and random input |
scripts/sonar.sh |
Clippy, coverage, ShellCheck, Semgrep, cargo-audit, and the quality gate |
scripts/analyse.sh |
the tests plus the model evaluation, over time; see Evaluating models |
Tests and evaluations run through scripts/sandbox.sh: loopback network only,
writes only to build and result folders, secrets unreadable, a throwaway HOME.