install
attest install: idempotent setup + --check doctor mode.
Step functions each return a StepResult; run_install() executes them in spec order and prints one aligned line per step. All subprocess calls go through the single _run() seam so tests can monkeypatch it.
This module implements the detection steps + pure-local fixes (uv, Ollama
reachability, models, .env, first data, warmup) and the agent-wiring steps
(mcp/skill/reasoning/cron), all gated on _find_agent_binary() finding
hermes-agent's own CLI (never our venv's console script).
Status
Bases: StrEnum
The closed set of step outcomes.
A StrEnum member IS a str -- every existing result.status == "OK"
comparison, f-string, and json.dumps call keeps working untouched -- but
ty now rejects a typo'd literal at check time instead of
_STATUS_LABEL[result.status] raising KeyError after all ten install
steps have already run.
StepResult
dataclass
One step's outcome: its Status plus a one-line human detail,
printed verbatim as run_install's aligned report line.
run_install(check=False, yes=False, now=False)
Run every step in spec order, print one aligned line each, and exit
nonzero iff any step is BROKEN -- the CLI entry point for attest install
and its --check doctor mode.
step_backends()
Which chat and embedding backends this process resolves to, and from where.
Hosts and model names only -- never a key. The question this answers is the
one a hosted machine raised: Attestation asked an embedder-only server for a
chat model, and nothing printed where it had got that URL. Pure resolution,
no request, so it costs nothing and changes nothing; it is BROKEN only when
the host opted into Hermes' model (ATTEST_LLM_FROM_HERMES) and Hermes has
none, which is a state every chat step would then fail in.
step_env_file(check=False)
Does .env exist; create it from .env.sample if not (skipped
entirely outside a checkout, where there is no sample to copy). Pins
ATTEST_DB to the checkout's database when nothing else fixes the path
(see _db_needs_pinning).
step_first_data(check=False, yes=False, now=False)
Does the database hold any items yet; ingest (and with now, tag)
with consent if not.
An absent database is reported rather than opened: get_db runs
CREATE TABLE as a side effect, so --check opening one would write a
~90KB file for a step whose whole point is to change nothing.
step_hosted_models(check=False)
For a non-Ollama LLM_BASE_URL: do the configured chat and embedding models actually answer?
Two tiny real requests (one embedding, one chat completion capped at one token) rather than a look at /v1/models: a hosted catalogue lists models an account cannot call (404) and models that have reached end of life (410), and only a request tells those apart from a working one. The embedding reply's width is checked against EMBED_DIMS too, since a model narrower than the stored width cannot be truncated to it.
No precondition on LLM_API_KEY: a LAN vLLM needs none, and a 401 from a
hosted endpoint says so in the detail. check changes nothing here --
both requests are read-only -- but is accepted so the step fits the
sequence. Sequence tests replace _hosted_probe; behaviour tests pass
it a transport.
step_ledger()
Is the run ledger reachable -- i.e. is RESEARCH_ROOT set and real?
Reports, never fixes, and NEVER returns BROKEN. The ledger is the one capability in the tool that needs no model and completes in about a second on a real corpus, and three reviews independently called it the strongest thing here -- yet it is gated behind an environment variable most people will never discover, which is the discoverability problem the scheduled-refresh spec named and then deliberately declined to solve with a prompt.
The reason a prompt was declined is the reason this is SKIPPED and not
BROKEN. --check exits nonzero iff some step is BROKEN, and ddd560b exists
precisely because skill_copy reported BROKEN over hermes-agent wiring a
self-hoster never asked for, forcing exit 1 on a working install. An
optional capability that fails the doctor is the same mistake in a
different costume. So this line informs and costs nothing: a [skipped]
naming the variable tells a reader the capability exists and how to turn it
on, while a user who only wants the feed still gets exit 0.
A RESEARCH_ROOT pointing at a directory that is gone is also SKIPPED, not BROKEN -- a stale setting from a moved workspace is not a broken install, and the detail says which of the two states it is so the reader can tell "never configured" from "configured, now wrong".
No check parameter: there is nothing here that could mutate anything, so
taking a flag it would ignore would imply otherwise.
step_mcp_wiring(agent, check=False)
Register the attestation MCP server with hermes-agent (and its per-surface tool restrictions) if not already wired, and verify the registration rather than trust it.
Confirms mcp add actually took (see the comment above the returncode
check) because this is the one step every other tool depends on: a
silent failure here means the agent has no attestation tools while the
installer reports success.
step_models(check=False, yes=False)
Are the configured chat + embed models installed; pull them with consent if not.
Every failure mode here is reported rather than left to abort the whole
run (see the comment above the shutil.which guard): a missing ollama
binary, an unreachable daemon, or a pull that fails partway all become a
StepResult instead of an uncaught exception that would blank the report
for every step after this one.
step_ollama_reachable()
Can the configured Ollama root be reached at all -- skipped, not broken, for a non-Ollama LLM_BASE_URL, which has nothing for this to check.
step_reasoning_override(agent, check=False)
Set agent.reasoning_overrides.<model>: none for a hermes3 chat model.
Scoped to hermes3 by fnmatch (see CLAUDE.md's llm.py note): the override exists for a model-specific quirk and every other chat model is skipped as not needing it, rather than the override being applied broadly on the theory that it is harmless elsewhere.
step_schedule(agent, check=False)
Write the refresh script and register hermes-agent's own cron job for it, detecting a hand-added crontab duplicate rather than reporting OK for a schedule that is silently racing itself.
Verifies agent cron create actually registered the job (see the
comment above the returncode check): one build parsed "cron" as a chat
prompt and exited 0 having registered nothing, so this checks
cron list afterwards rather than trusting the exit code.
step_skill_copy(agent=None, check=False)
Sync every bundled skill into every skills tree Hermes reads,
byte-for-byte, skipping unchanged files -- and disable the superseded
research-provenance monolith wherever it is still enabled.
Skipped when no hermes-agent binary is found, the same as the other
three agent-wiring steps (mcp_wiring, reasoning_override, schedule): a
fresh, agent-less home used to report BROKEN here on every --check
run, which alone forced exit 1 for a self-hoster who only wants the
local feed/ledger and never asked for hermes-agent wiring. These skill
files exist to be read by a hermes-agent session, so agent=None
(the default) now means "nothing to sync" rather than "sync, then
report broken forever." Pass a real agent path to actually sync.
Skipped -- not broken -- when the skills are not bundled with this install (see the comment below): this is a fallback lane, and an odd packaging mode losing it should not fail the rest of the run.
step_uv()
Is uv on PATH -- the one dependency this installer cannot fix itself.
step_warmup(check=False)
Pin chat + embed models in VRAM, skipped under --check (see below) and for a non-Ollama backend, which has nothing to pin.