Root AGENTS.md § Code Shape Rules replaces "module-level constants are fine — they cache after _apply_profile_override() sets HERMES_HOME" (true for `hermes -p x <cmd>`, inverted under the multiplex gateway and the Desktop/dashboard `serve` backend, where os.environ holds the LAUNCH profile) with the invariant: a profile = home + secret scope + terminal scope, bound per profile ACTIVITY, and every execution point with no turn on the stack binds it explicitly. Names the real seams: gateway/run.py::_profile_runtime_scope, tui_gateway @_profile_scoped + _session_profile_runtime_scope (+ _profile_runtime_scope_tokens, launch_profile_policy -> set_multiplex_active), cron/scheduler_provider.py::_profile_cron_scope, gateway/run_agent_cache.py::_run_release_in_profile_scope, tools/environments/local.py:: served_profile_child_env, agent/memory_provider.py::spawn_context_thread. Adds a routing-table row for profiles / multiplex / secret scope. Area AGENTS.md paragraphs, one per seam, for gateway/ (activity-not-turn binding, hooks per profile, adapter YAML never reaches os.environ, unserved shared-ingress reported via _note_unserved_secondary_platform + needs_attention at the single writer), tui_gateway/ (RPC binding is home AND secret AND terminal; HOME-only is half-bound; teardown chokepoint), cron/ (per-home tick lock, ticker scope incl. pre-loop code, kanban notifier routing, worker liveness by (pid, worker_started_at) fingerprint, descendant fence as a path), hermes_cli/ (DEFAULT_CONFIG key <-> reader parity, service-install matrix, -p vs multiplex home binding), tools/ (check_fn reads through get_secret and is cached per hermes_home_key, one env builder per spawn, MCP trust per profile), plugins/ (lifecycle hooks are bound by the caller; never cache the home from initialize()), apps/desktop/src/ (pooled serve per (connection, profile); remote topologies), agent/ (end-of-session flush is caller-bound; set_multiplex_active gates fail-closed). Corrects the statements the multiplex model made wrong, in the same PR: root module-constant sentence; hermes_cli "sets HERMES_HOME before any import" (+ cli-internals.md); ADDING_A_PLATFORM.md §2 raw os.getenv loader (now an _ENV_STEPS row through config.py::_getenv) and §4 platform_env_map in gateway/run.py (now _PLATFORM_ALLOWLIST_ENV in pairing.py + registry allowed_users_env); platform_registry.py "may set os.environ (guard with not os.getenv)"; cron/AGENTS.md hardcoded ~/.hermes/cron/.tick.lock; gateway-internals.md agent:main as THE key format, ~/.hermes/hooks/, single-profile `gateway stop`, plus a new "Multiplexed profiles" section; tools/AGENTS.md os.getenv check_fn sample; "installed per turn" wording; "one temp HERMES_HOME" E2E wording; multi-profile-gateways.md intro lists system units, Windows tasks, s6 and the Desktop backend.
86 lines
5.5 KiB
Markdown
86 lines
5.5 KiB
Markdown
---
|
|
sidebar_position: 15
|
|
title: "CLI Internals"
|
|
description: "How hermes_cli is shaped: slash dispatch, config loaders, the skin engine, the transactional update pipeline, and process-identity rules"
|
|
---
|
|
|
|
# CLI Internals
|
|
|
|
Companion to `hermes_cli/AGENTS.md` (the rules) — this page holds the longer explanations.
|
|
|
|
## Update pipeline
|
|
|
|
The stage-by-stage contract (`plan → snapshot → apply → restart-per-kind → verify → report`) and the
|
|
field failure each stage guards are documented in `hermes_cli/AGENTS.md`; user-facing behaviour
|
|
(receipts, `--plan`, snapshot modes) is in [Updating](../getting-started/updating.md).
|
|
|
|
The systemd blunt-restart fallback waits for the unit's `TimeoutStopUSec` plus
|
|
`TimeoutStartUSec`, with 15 seconds of client-side slack. It reads the target unit
|
|
in the same manager scope as the restart; both the initial attempt and retry use
|
|
this budget, including the catch-up restart after an interrupted update.
|
|
A start after a graceful drain uses only the start budget plus slack.
|
|
A missing, unparseable, or infinite phase limit falls back to 90 seconds
|
|
for that phase, keeping unattended updates bounded. Timing out the `systemctl`
|
|
client does **not** cancel the manager's transaction. Custom multi-command stop
|
|
chains or `EXTEND_TIMEOUT_USEC` can still outlast this estimate; a real timeout
|
|
remains an incomplete restart, and successful commands still require the existing
|
|
service-health and fleet-version verification. Raw numeric `*USec` values are
|
|
microseconds, while formatted values use systemd's fixed units, including days,
|
|
weeks, months and years. The combined timeout is capped below the native signed
|
|
32-bit millisecond poll limit (with rounding headroom), so exceptionally long
|
|
unit limits cannot overflow subprocess polling. Zero/unknown/infinite phase
|
|
limits use the bounded fallback. This does not change active-turn drain settings.
|
|
|
|
## Process identity: never infer it from argv substrings
|
|
|
|
The bug class behind ~10 fleet-update issues (#90778, #87594, #78089, #76129, #91964, ...):
|
|
classifying a process by `"serve" in cmdline` or similar. `kanban --preserve-cache` contains
|
|
"serve"; a flag VALUE can equal a subcommand (`-m dashboard serve`); truncated cmdlines hide the real
|
|
subcommand. Rules:
|
|
|
|
- Use the canonical matchers: `gateway.status.looks_like_gateway_command_line` (gateway run),
|
|
`hermes_cli.update_cmd._hermes_holder_subcommand` (top-level subcommand of any Hermes argv). Never
|
|
hand-roll token scans.
|
|
- Flag sets must be DERIVED from the parser (`_holder_value_flags()` introspects
|
|
`build_top_level_parser()`), never hand-written lists — they drift.
|
|
- Never blanket-exclude ancestors from process scans: when `/update` runs as the gateway's child, a
|
|
gateway ancestor must stay visible to the pause machinery (#87594). Exclude interactive ancestry,
|
|
carve out gateway-shaped ancestors.
|
|
- Match on FULL cmdlines; truncate only at display time (#78089).
|
|
- Before adding any new scan heuristic, read #92091 — the gateway control socket replaces scans as
|
|
the primary coordination mechanism; scans are the fallback layer for old/crashed processes.
|
|
|
|
## Skin engine — what skins customize
|
|
|
|
| Element | Skin key | Used by |
|
|
|---|---|---|
|
|
| Banner panel border / title / section headers / dim / body | `colors.banner_border`, `banner_title`, `banner_accent`, `banner_dim`, `banner_text` | `banner.py` |
|
|
| Response box border | `colors.response_border` | `cli.py` |
|
|
| Spinner faces (waiting / thinking) | `spinner.waiting_faces`, `spinner.thinking_faces` | `display.py` |
|
|
| Spinner verbs / wings (optional) | `spinner.thinking_verbs`, `spinner.wings` | `display.py` |
|
|
| Tool output prefix / per-tool emojis | `tool_prefix`, `tool_emojis` | `display.py` → `get_tool_emoji()` |
|
|
| Agent name / welcome / response label / prompt symbol | `branding.agent_name`, `welcome`, `response_label`, `prompt_symbol` | `banner.py`, `cli.py` |
|
|
|
|
Built-in skins (`_BUILTIN_SKINS` in `hermes_cli/skin_engine.py`): `default` (classic gold/kawaii),
|
|
`ares` (crimson/bronze with custom spinner wings), `mono` (grayscale), `slate` (cool blue). Add a
|
|
built-in as a dict entry `{"name", "description", "colors", "spinner", "branding", "tool_prefix"}`.
|
|
User skins are `~/.hermes/skins/<name>.yaml` with the same keys, activated with `/skin <name>` or
|
|
`display.skin: <name>`; the full YAML template is in the
|
|
[Skins & Themes](../user-guide/features/skins.md) user guide.
|
|
|
|
## Profiles: multi-instance support
|
|
|
|
Hermes supports profiles — fully isolated instances, each with its own `HERMES_HOME` (config, API
|
|
keys, memory, sessions, skills, gateway). For single-profile commands (`hermes -p x <cmd>`),
|
|
`_apply_profile_override()` in `hermes_cli/main.py` sets `HERMES_HOME` before any module imports, so
|
|
every `get_hermes_home()` reference scopes to the active profile. The multiplex gateway and the
|
|
Desktop/dashboard `serve` backend serve several profiles from one process instead: the active
|
|
profile is a contextvar override bound per activity, `os.environ["HERMES_HOME"]` stays the launch
|
|
profile's, and a module-level constant derived from the home freezes to that launch profile (see
|
|
[Gateway Internals § Multiplexed profiles](./gateway-internals.md#multiplexed-profiles)). Profile
|
|
operations are HOME-anchored (`_get_profiles_root()` returns
|
|
`Path.home() / ".hermes" / "profiles"`, not `get_hermes_home() / "profiles"`) so
|
|
`hermes -p coder profile list` sees all profiles regardless of which one is active — intentional.
|
|
Profile-safe coding rules are in the root `AGENTS.md`; multiplex secret-scope rules in
|
|
`gateway/AGENTS.md`.
|