Skip to content

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.