From 86100e5321b80b7df0f3652457106c5df67b8743 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Wed, 2 Sep 2026 15:21:52 -0700 Subject: [PATCH 1/3] refactor(config_defaults): build OPTIONAL_ENV_VARS via _env() and collapse scalar sub-dicts --- hermes_cli/config_defaults.py | 1689 +++++++++------------------------ 1 file changed, 453 insertions(+), 1236 deletions(-) diff --git a/hermes_cli/config_defaults.py b/hermes_cli/config_defaults.py index 0dc0776bcb..2bd2a35158 100644 --- a/hermes_cli/config_defaults.py +++ b/hermes_cli/config_defaults.py @@ -22,9 +22,7 @@ DEFAULT_CONFIG = { }, # Soft file-descriptor limit for long-running Hermes server processes. # Clamped to the OS hard limit; 0/false/null disables the adjustment. - "runtime": { - "nofile_soft_limit": 4096, - }, + "runtime": {"nofile_soft_limit": 4096}, # Global active chat session cap across CLI, TUI/dashboard, and messaging. # None/0 = unbounded. "max_concurrent_sessions": None, @@ -404,10 +402,7 @@ DEFAULT_CONFIG = { # values (typo, NaN, Inf, non-positive poll) warn and fall back to # the default instead of crashing startup or silently disabling the # watchdog. See agent/turn_liveness.py. - "turn_liveness": { - "timeout_s": 600.0, - "poll_s": 15.0, - }, + "turn_liveness": {"timeout_s": 600.0, "poll_s": 15.0}, }, "terminal": { @@ -694,10 +689,7 @@ DEFAULT_CONFIG = { # Local API registration additionally requires the API server bearer # key. developer_mode gates the privileged capabilities # (browser_cdp / browser_evaluate) — never negotiable without it. - "extension_control": { - "enabled": False, - "developer_mode": False, - }, + "extension_control": {"enabled": False, "developer_mode": False}, }, # Filesystem checkpoints — automatic snapshots before destructive file ops. @@ -812,11 +804,7 @@ DEFAULT_CONFIG = { # being clamped (default 2000). # - max_line_length: per-line cap applied when read_file emits a # line-numbered view (default 2000 chars). - "tool_output": { - "max_bytes": 50_000, - "max_lines": 2000, - "max_line_length": 2000, - }, + "tool_output": {"max_bytes": 50000, "max_lines": 2000, "max_line_length": 2000}, # Tool loop guardrails nudge models when they repeat failed or # non-progressing tool calls. Soft warnings are always-on by default; @@ -828,11 +816,7 @@ DEFAULT_CONFIG = { # is present to /stop a model that ignores loop warnings); interactive # cli/tui/desktop/acp stay warning-only unless hard_stop_enabled. "non_interactive_hard_stop_enabled": True, - "warn_after": { - "exact_failure": 2, - "same_tool_failure": 3, - "idempotent_no_progress": 2, - }, + "warn_after": {"exact_failure": 2, "same_tool_failure": 3, "idempotent_no_progress": 2}, "hard_stop_after": { "exact_failure": 5, "same_tool_failure": 8, @@ -1089,9 +1073,7 @@ DEFAULT_CONFIG = { # cache_ttl: "5m" or "1h" (Anthropic-supported tiers). Other non-falsy # values are silently ignored. Falsy values (false, null, "off", # "disabled", "no", "none") disable prompt caching entirely. - "prompt_caching": { - "cache_ttl": "5m", - }, + "prompt_caching": {"cache_ttl": "5m"}, # OpenRouter-specific settings. # response_cache: enable OpenRouter response caching (X-OpenRouter-Cache header). @@ -1108,11 +1090,7 @@ DEFAULT_CONFIG = { # pick the strongest available coder (router's documented default # when the plugins block is omitted). # See: https://openrouter.ai/docs/guides/routing/routers/pareto-router - "openrouter": { - "response_cache": True, - "response_cache_ttl": 300, - "min_coding_score": 0.65, - }, + "openrouter": {"response_cache": True, "response_cache_ttl": 300, "min_coding_score": 0.65}, # AWS Bedrock provider configuration. # Only used when model.provider is "bedrock". @@ -1829,10 +1807,7 @@ DEFAULT_CONFIG = { # disabled. ``scope`` is the capability label attached to the verified # principal; ``min_secret_chars`` is the entropy bar (url-safe-b64 # chars; 43 ~= 256 bits). - "drain_auth": { - "scope": "drain", - "min_secret_chars": 43, - }, + "drain_auth": {"scope": "drain", "min_secret_chars": 43}, # Public URL override (env: ``HERMES_DASHBOARD_PUBLIC_URL``). # When set, this is the complete authority — scheme + host + # optional path prefix (e.g. ``https://example.com/hermes``) — @@ -1914,10 +1889,7 @@ DEFAULT_CONFIG = { "model": "voxtral-mini-tts-2603", "voice_id": "c69964a6-ab8b-4f8a-9465-ec0925096ec8", # Paul - Neutral }, - "minimax": { - "model": "speech-02-hd", - "voice_id": "English_expressive_narrator", - }, + "minimax": {"model": "speech-02-hd", "voice_id": "English_expressive_narrator"}, "kittentts": { "model": "KittenML/kitten-tts-nano-0.8-int8", # nano 25MB; micro 41MB; mini 80MB "voice": "Jasper", @@ -2074,11 +2046,7 @@ DEFAULT_CONFIG = { }, }, - "human_delay": { - "mode": "off", - "min_ms": 800, - "max_ms": 2500, - }, + "human_delay": {"mode": "off", "min_ms": 800, "max_ms": 2500}, # Context engine -- controls how the context window is managed when # approaching the model's token limit. @@ -2724,10 +2692,7 @@ DEFAULT_CONFIG = { # denies unless transport_fallback is explicitly set to "builtin". # This is presentation only: plugins cannot detect, suppress, or # auto-approve commands outside a correlated human response. - "approval": { - "transport": "builtin", - "transport_fallback": "deny", - }, + "approval": {"transport": "builtin", "transport_fallback": "deny"}, # Writes to agent-instruction files (AGENTS.md/CLAUDE.md/SOUL.md/ # .cursorrules, project-local .hermes config) always require human # approval — even under auto-approve/yolo. Extra patterns are @@ -2738,11 +2703,7 @@ DEFAULT_CONFIG = { "tirith_path": "tirith", "tirith_timeout": 5, "tirith_fail_open": True, - "website_blocklist": { - "enabled": False, - "domains": [], - "shared_files": [], - }, + "website_blocklist": {"enabled": False, "domains": [], "shared_files": []}, # Acknowledged supply-chain security advisories. Each entry is the # ID of an advisory the user has read and acted on (uninstalled the # compromised package, rotated credentials). Acked advisories no @@ -3223,13 +3184,7 @@ DEFAULT_CONFIG = { # OTLP destination. headers_env maps header names to ENVIRONMENT # VARIABLE NAMES (never secret values); values are read from the # environment at export time. - "export": { - "otlp": { - "enabled": False, - "endpoint": "", - "headers_env": {}, - }, - }, + "export": {"otlp": {"enabled": False, "endpoint": "", "headers_env": {}}}, }, # Gateway settings — control how messaging platforms (Telegram, Discord, @@ -3309,9 +3264,7 @@ DEFAULT_CONFIG = { # it wakes on the connector's wakeUrl poke. This is the idle TIMEOUT only # — whether the feature is enabled at all is the Labs toggle, never a # config key (decisions.md D2/D11). 0/negative falls back to the default. - "scale_to_zero": { - "idle_timeout_minutes": 2, - }, + "scale_to_zero": {"idle_timeout_minutes": 2}, # Auto-resume restart-loop breaker (#30719, defense-3). When the # gateway is killed mid-turn (SIGTERM) and revived by a supervisor @@ -3334,11 +3287,7 @@ DEFAULT_CONFIG = { # #81642 (stall -> ~90s liveness-watchdog hard-exit -> respawn -> # auto-resume replays the same session), which also makes # `hermes update` hang because it can never drain the gateway. - "restart_loop_guard": { - "max_restarts": 3, - "window_seconds": 60, - "max_gap_seconds": 300, - }, + "restart_loop_guard": {"max_restarts": 3, "window_seconds": 60, "max_gap_seconds": 300}, # Portable respawn-storm circuit breaker (complements # ``restart_loop_guard`` above). Counts gateway (re)starts in a sliding @@ -3348,10 +3297,7 @@ DEFAULT_CONFIG = { # ``max_starts <= 0`` disables the breaker. The env vars # ``HERMES_GATEWAY_MAX_STARTS`` / ``HERMES_GATEWAY_START_WINDOW_S`` # override these defaults for escape-hatch use. - "respawn_storm": { - "max_starts": 5, - "window_seconds": 120, - }, + "respawn_storm": {"max_starts": 5, "window_seconds": 120}, # Inject a human-readable timestamp prefix (e.g. # "[Tue 2026-04-28 13:40:53 CEST]") onto user messages IN THE MODEL'S @@ -3360,9 +3306,7 @@ DEFAULT_CONFIG = { # Persisted transcripts always stay clean (the timestamp is stored as # message metadata regardless of this toggle), so turning it on later # surfaces send-times for past messages too. - "message_timestamps": { - "enabled": False, - }, + "message_timestamps": {"enabled": False}, # Maximum bytes for an inbound image / audio / video payload the # gateway will buffer into memory and cache to disk. Inbound media is @@ -3810,10 +3754,7 @@ DEFAULT_CONFIG = { # material under ~/.hermes/cache/. If a later startup cannot reach # Bitwarden due to NETWORK/TIMEOUT, Hermes may use this encrypted # cache for up to max_stale_seconds. Auth failures do not fall back. - "encrypted_cache": { - "enabled": False, - "max_stale_seconds": 0, - }, + "encrypted_cache": {"enabled": False, "max_stale_seconds": 0}, # When True, BSM values overwrite existing env vars. Default # True because the point of using BSM is centralized rotation — # if .env had the final say, rotating in Bitwarden wouldn't @@ -4108,1183 +4049,467 @@ DEFAULT_CONFIG = { "_config_version": 40, } + +def _env(description, prompt, **keys): + """One OPTIONAL_ENV_VARS entry; keyword order is preserved as dict key order.""" + return {"description": description, "prompt": prompt, **keys} + # Optional environment variables that enhance functionality OPTIONAL_ENV_VARS = { # ── Provider (handled in provider selection, not shown in checklists) ── - "NOUS_BASE_URL": { - "description": "Nous Portal base URL override", - "prompt": "Nous Portal base URL (leave empty for default)", - "url": None, - "password": False, - "category": "provider", - "advanced": True, - }, - "OPENROUTER_API_KEY": { - "description": "OpenRouter API key (for vision, web scraping helpers, and MoA)", - "prompt": "OpenRouter API key", - "url": "https://openrouter.ai/keys", - "password": True, - "tools": ["vision_analyze"], - "category": "provider", - "advanced": True, - }, - "GOOGLE_API_KEY": { - "description": "Google AI Studio API key (also recognized as GEMINI_API_KEY)", - "prompt": "Google AI Studio API key", - "url": "https://aistudio.google.com/app/apikey", - "password": True, - "category": "provider", - "advanced": True, - }, - "GEMINI_API_KEY": { - "description": "Google AI Studio API key (alias for GOOGLE_API_KEY)", - "prompt": "Gemini API key", - "url": "https://aistudio.google.com/app/apikey", - "password": True, - "category": "provider", - "advanced": True, - }, - "GEMINI_BASE_URL": { - "description": "Google AI Studio base URL override", - "prompt": "Gemini base URL (leave empty for default)", - "url": None, - "password": False, - "category": "provider", - "advanced": True, - }, - "VERTEX_CREDENTIALS_PATH": { - "description": "Path to a Google Cloud service account JSON for Vertex AI (Gemini). " - "Vertex uses OAuth2, not a static API key — this points at the " - "credentials Hermes mints short-lived tokens from. Falls back to " - "GOOGLE_APPLICATION_CREDENTIALS, then to ADC (gcloud auth " - "application-default login). Set project/region under vertex: in config.yaml.", - "prompt": "Vertex service account JSON path (leave empty to use ADC / GOOGLE_APPLICATION_CREDENTIALS)", - "url": "https://cloud.google.com/iam/docs/keys-create-delete", - "password": False, - "category": "provider", - "advanced": True, - }, - "XAI_API_KEY": { - "description": "xAI API key", - "prompt": "xAI API key", - "url": "https://console.x.ai/", - "password": True, - "category": "provider", - "advanced": True, - }, - "XAI_BASE_URL": { - "description": "xAI base URL override", - "prompt": "xAI base URL (leave empty for default)", - "url": None, - "password": False, - "category": "provider", - "advanced": True, - }, - "NVIDIA_API_KEY": { - "description": "NVIDIA NIM API key (build.nvidia.com or local NIM endpoint)", - "prompt": "NVIDIA NIM API key", - "url": "https://build.nvidia.com/", - "password": True, - "category": "provider", - "advanced": True, - }, - "NVIDIA_BASE_URL": { - "description": "NVIDIA NIM base URL override (e.g. http://localhost:8000/v1 for local NIM)", - "prompt": "NVIDIA NIM base URL (leave empty for default)", - "url": None, - "password": False, - "category": "provider", - "advanced": True, - }, - "LM_API_KEY": { - "description": "LM Studio bearer token for auth-enabled local servers", - "prompt": "LM Studio API key / bearer token", - "url": None, - "password": True, - "category": "provider", - "advanced": True, - }, - "LM_BASE_URL": { - "description": "LM Studio base URL override", - "prompt": "LM Studio base URL (leave empty for default)", - "url": None, - "password": False, - "category": "provider", - "advanced": True, - }, - "GLM_API_KEY": { - "description": "Z.AI / GLM API key (also recognized as ZAI_API_KEY / Z_AI_API_KEY)", - "prompt": "Z.AI / GLM API key", - "url": "https://z.ai/", - "password": True, - "category": "provider", - "advanced": True, - }, - "ZAI_API_KEY": { - "description": "Z.AI API key (alias for GLM_API_KEY)", - "prompt": "Z.AI API key", - "url": "https://z.ai/", - "password": True, - "category": "provider", - "advanced": True, - }, - "Z_AI_API_KEY": { - "description": "Z.AI API key (alias for GLM_API_KEY)", - "prompt": "Z.AI API key", - "url": "https://z.ai/", - "password": True, - "category": "provider", - "advanced": True, - }, - "GLM_BASE_URL": { - "description": "Z.AI / GLM base URL override", - "prompt": "Z.AI / GLM base URL (leave empty for default)", - "url": None, - "password": False, - "category": "provider", - "advanced": True, - }, - "KIMI_API_KEY": { - "description": "Kimi / Moonshot API key", - "prompt": "Kimi API key", - "url": "https://platform.moonshot.cn/", - "password": True, - "category": "provider", - "advanced": True, - }, - "KIMI_BASE_URL": { - "description": "Kimi / Moonshot base URL override", - "prompt": "Kimi base URL (leave empty for default)", - "url": None, - "password": False, - "category": "provider", - "advanced": True, - }, - "KIMI_CN_API_KEY": { - "description": "Kimi / Moonshot China API key", - "prompt": "Kimi (China) API key", - "url": "https://platform.moonshot.cn/", - "password": True, - "category": "provider", - "advanced": True, - }, - "STEPFUN_API_KEY": { - "description": "StepFun Step Plan API key", - "prompt": "StepFun Step Plan API key", - "url": "https://platform.stepfun.com/", - "password": True, - "category": "provider", - "advanced": True, - }, - "STEPFUN_BASE_URL": { - "description": "StepFun Step Plan base URL override", - "prompt": "StepFun Step Plan base URL (leave empty for default)", - "url": None, - "password": False, - "category": "provider", - "advanced": True, - }, - "ARCEEAI_API_KEY": { - "description": "Arcee AI API key", - "prompt": "Arcee AI API key", - "url": "https://chat.arcee.ai/", - "password": True, - "category": "provider", - "advanced": True, - }, - "ARCEE_BASE_URL": { - "description": "Arcee AI base URL override", - "prompt": "Arcee base URL (leave empty for default)", - "url": None, - "password": False, - "category": "provider", - "advanced": True, - }, - "GMI_API_KEY": { - "description": "GMI Cloud API key", - "prompt": "GMI Cloud API key", - "url": "https://www.gmicloud.ai/", - "password": True, - "category": "provider", - "advanced": True, - }, - "GMI_BASE_URL": { - "description": "GMI Cloud base URL override", - "prompt": "GMI Cloud base URL (leave empty for default)", - "url": None, - "password": False, - "category": "provider", - "advanced": True, - }, - "ACTUAL_API_KEY": { - "description": "Actual Computer inference key (ac_...)", - "prompt": "Actual Computer inference key", - "url": "https://actual.inc/user/keys", - "password": True, - "category": "provider", - "advanced": True, - }, - "ACTUAL_BASE_URL": { - "description": "Actual Computer base URL override (set to http://127.0.0.1:8080 for the local offline daemon)", - "prompt": "Actual Computer base URL (leave empty for hosted relay)", - "url": None, - "password": False, - "category": "provider", - "advanced": True, - }, - "FIREWORKS_API_KEY": { - "description": "Fireworks AI API key", - "prompt": "Fireworks AI API key", - "url": "https://app.fireworks.ai/settings/users/api-keys", - "password": True, - "category": "provider", - "advanced": True, - }, - "MINIMAX_API_KEY": { - "description": "MiniMax API key (international)", - "prompt": "MiniMax API key", - "url": "https://www.minimax.io/", - "password": True, - "category": "provider", - "advanced": True, - }, - "MINIMAX_BASE_URL": { - "description": "MiniMax base URL override", - "prompt": "MiniMax base URL (leave empty for default)", - "url": None, - "password": False, - "category": "provider", - "advanced": True, - }, - "MINIMAX_CN_API_KEY": { - "description": "MiniMax API key (China endpoint)", - "prompt": "MiniMax (China) API key", - "url": "https://www.minimaxi.com/", - "password": True, - "category": "provider", - "advanced": True, - }, - "MINIMAX_CN_BASE_URL": { - "description": "MiniMax (China) base URL override", - "prompt": "MiniMax (China) base URL (leave empty for default)", - "url": None, - "password": False, - "category": "provider", - "advanced": True, - }, - "DEEPSEEK_API_KEY": { - "description": "DeepSeek API key for direct DeepSeek access", - "prompt": "DeepSeek API Key", - "url": "https://platform.deepseek.com/api_keys", - "password": True, - "category": "provider", - }, - "DEEPSEEK_BASE_URL": { - "description": "Custom DeepSeek API base URL (advanced)", - "prompt": "DeepSeek Base URL", - "url": "", - "password": False, - "category": "provider", - }, - "DASHSCOPE_API_KEY": { - "description": "Alibaba Cloud DashScope API key (Qwen + multi-provider models)", - "prompt": "DashScope API Key", - "url": "https://modelstudio.console.alibabacloud.com/", - "password": True, - "category": "provider", - }, - "DASHSCOPE_BASE_URL": { - "description": "Custom DashScope base URL (default: coding-intl OpenAI-compat endpoint)", - "prompt": "DashScope Base URL", - "url": "", - "password": False, - "category": "provider", - "advanced": True, - }, - "HERMES_QWEN_BASE_URL": { - "description": "Qwen Portal base URL override (default: https://portal.qwen.ai/v1)", - "prompt": "Qwen Portal base URL (leave empty for default)", - "url": None, - "password": False, - "category": "provider", - "advanced": True, - }, - "OPENCODE_ZEN_API_KEY": { - "description": "OpenCode Zen API key (pay-as-you-go access to curated models)", - "prompt": "OpenCode Zen API key", - "url": "https://opencode.ai/auth", - "password": True, - "category": "provider", - "advanced": True, - }, - "COMMANDCODE_API_KEY": { - "description": "CommandCode API key (GOAT/Pro/Max/Provider plans — 30+ models via one key)", - "prompt": "CommandCode API key", - "url": "https://commandcode.ai/studio/", - "password": True, - "category": "provider", - "advanced": True, - }, - "OPENCODE_ZEN_BASE_URL": { - "description": "OpenCode Zen base URL override", - "prompt": "OpenCode Zen base URL (leave empty for default)", - "url": None, - "password": False, - "category": "provider", - "advanced": True, - }, - "OPENCODE_GO_API_KEY": { - "description": "OpenCode Go API key ($10/month subscription for open models)", - "prompt": "OpenCode Go API key", - "url": "https://opencode.ai/auth", - "password": True, - "category": "provider", - "advanced": True, - }, - "OPENCODE_GO_BASE_URL": { - "description": "OpenCode Go base URL override", - "prompt": "OpenCode Go base URL (leave empty for default)", - "url": None, - "password": False, - "category": "provider", - "advanced": True, - }, - "HF_TOKEN": { - "description": "Hugging Face token for Inference Providers (20+ open models via router.huggingface.co)", - "prompt": "Hugging Face Token", - "url": "https://huggingface.co/settings/tokens", - "password": True, - "category": "provider", - }, - "HF_BASE_URL": { - "description": "Hugging Face Inference Providers base URL override", - "prompt": "HF base URL (leave empty for default)", - "url": None, - "password": False, - "category": "provider", - "advanced": True, - }, - "OLLAMA_API_KEY": { - "description": "Ollama Cloud API key (ollama.com — cloud-hosted open models)", - "prompt": "Ollama Cloud API key", - "url": "https://ollama.com/settings", - "password": True, - "category": "provider", - "advanced": True, - }, - "OLLAMA_BASE_URL": { - "description": "Ollama Cloud base URL override (default: https://ollama.com/v1)", - "prompt": "Ollama base URL (leave empty for default)", - "url": None, - "password": False, - "category": "provider", - "advanced": True, - }, - "XIAOMI_API_KEY": { - "description": "Xiaomi MiMo API key for MiMo models (mimo-v2.5-pro, mimo-v2.5, mimo-v2-pro, mimo-v2-omni, mimo-v2-flash)", - "prompt": "Xiaomi MiMo API Key", - "url": "https://platform.xiaomimimo.com", - "password": True, - "category": "provider", - }, - "XIAOMI_BASE_URL": { - "description": "Xiaomi MiMo base URL override (default: https://api.xiaomimimo.com/v1)", - "prompt": "Xiaomi base URL (leave empty for default)", - "url": None, - "password": False, - "category": "provider", - "advanced": True, - }, - "UPSTAGE_API_KEY": { - "description": "Upstage API key for Solar LLM models", - "prompt": "Upstage API Key", - "url": "https://console.upstage.ai/api-keys", - "password": True, - "category": "provider", - }, - "UPSTAGE_BASE_URL": { - "description": "Upstage base URL override (default: https://api.upstage.ai/v1)", - "prompt": "Upstage base URL (leave empty for default)", - "url": None, - "password": False, - "category": "provider", - "advanced": True, - }, - "AWS_REGION": { - "description": "AWS region for Bedrock API calls (e.g. us-east-1, eu-central-1)", - "prompt": "AWS Region", - "url": "https://docs.aws.amazon.com/bedrock/latest/userguide/bedrock-regions.html", - "password": False, - "category": "provider", - "advanced": True, - }, - "AWS_PROFILE": { - "description": "AWS named profile for Bedrock authentication (from ~/.aws/credentials)", - "prompt": "AWS Profile", - "url": None, - "password": False, - "category": "provider", - "advanced": True, - }, - "AZURE_FOUNDRY_API_KEY": { - "description": "Azure Foundry API key for custom Azure endpoints", - "prompt": "Azure Foundry API Key", - "url": "https://ai.azure.com/", - "password": True, - "category": "provider", - }, - "AZURE_FOUNDRY_BASE_URL": { - "description": "Azure Foundry base URL (set via 'hermes model' for endpoint-specific config)", - "prompt": "Azure Foundry base URL", - "url": None, - "password": False, - "category": "provider", - "advanced": True, - }, + "NOUS_BASE_URL": _env("Nous Portal base URL override", "Nous Portal base URL (leave empty for default)", + url=None, password=False, category="provider", advanced=True), + "OPENROUTER_API_KEY": _env("OpenRouter API key (for vision, web scraping helpers, and MoA)", + "OpenRouter API key", url="https://openrouter.ai/keys", password=True, tools=["vision_analyze"], + category="provider", advanced=True), + "GOOGLE_API_KEY": _env("Google AI Studio API key (also recognized as GEMINI_API_KEY)", + "Google AI Studio API key", url="https://aistudio.google.com/app/apikey", password=True, + category="provider", advanced=True), + "GEMINI_API_KEY": _env("Google AI Studio API key (alias for GOOGLE_API_KEY)", "Gemini API key", + url="https://aistudio.google.com/app/apikey", password=True, category="provider", advanced=True), + "GEMINI_BASE_URL": _env("Google AI Studio base URL override", "Gemini base URL (leave empty for default)", + url=None, password=False, category="provider", advanced=True), + "VERTEX_CREDENTIALS_PATH": _env( + "Path to a Google Cloud service account JSON for Vertex AI (Gemini). Vertex uses OAuth2, not a " + "static API key — this points at the credentials Hermes mints short-lived tokens from. Falls back " + "to GOOGLE_APPLICATION_CREDENTIALS, then to ADC (gcloud auth application-default login). Set " + "project/region under vertex: in config.yaml.", + "Vertex service account JSON path (leave empty to use ADC / GOOGLE_APPLICATION_CREDENTIALS)", + url="https://cloud.google.com/iam/docs/keys-create-delete", password=False, category="provider", + advanced=True), + "XAI_API_KEY": _env("xAI API key", "xAI API key", url="https://console.x.ai/", password=True, + category="provider", advanced=True), + "XAI_BASE_URL": _env("xAI base URL override", "xAI base URL (leave empty for default)", url=None, + password=False, category="provider", advanced=True), + "NVIDIA_API_KEY": _env("NVIDIA NIM API key (build.nvidia.com or local NIM endpoint)", + "NVIDIA NIM API key", url="https://build.nvidia.com/", password=True, category="provider", + advanced=True), + "NVIDIA_BASE_URL": _env("NVIDIA NIM base URL override (e.g. http://localhost:8000/v1 for local NIM)", + "NVIDIA NIM base URL (leave empty for default)", url=None, password=False, category="provider", + advanced=True), + "LM_API_KEY": _env("LM Studio bearer token for auth-enabled local servers", + "LM Studio API key / bearer token", url=None, password=True, category="provider", advanced=True), + "LM_BASE_URL": _env("LM Studio base URL override", "LM Studio base URL (leave empty for default)", + url=None, password=False, category="provider", advanced=True), + "GLM_API_KEY": _env("Z.AI / GLM API key (also recognized as ZAI_API_KEY / Z_AI_API_KEY)", + "Z.AI / GLM API key", url="https://z.ai/", password=True, category="provider", advanced=True), + "ZAI_API_KEY": _env("Z.AI API key (alias for GLM_API_KEY)", "Z.AI API key", url="https://z.ai/", + password=True, category="provider", advanced=True), + "Z_AI_API_KEY": _env("Z.AI API key (alias for GLM_API_KEY)", "Z.AI API key", url="https://z.ai/", + password=True, category="provider", advanced=True), + "GLM_BASE_URL": _env("Z.AI / GLM base URL override", "Z.AI / GLM base URL (leave empty for default)", + url=None, password=False, category="provider", advanced=True), + "KIMI_API_KEY": _env("Kimi / Moonshot API key", "Kimi API key", url="https://platform.moonshot.cn/", + password=True, category="provider", advanced=True), + "KIMI_BASE_URL": _env("Kimi / Moonshot base URL override", "Kimi base URL (leave empty for default)", + url=None, password=False, category="provider", advanced=True), + "KIMI_CN_API_KEY": _env("Kimi / Moonshot China API key", "Kimi (China) API key", + url="https://platform.moonshot.cn/", password=True, category="provider", advanced=True), + "STEPFUN_API_KEY": _env("StepFun Step Plan API key", "StepFun Step Plan API key", + url="https://platform.stepfun.com/", password=True, category="provider", advanced=True), + "STEPFUN_BASE_URL": _env("StepFun Step Plan base URL override", + "StepFun Step Plan base URL (leave empty for default)", url=None, password=False, category="provider", + advanced=True), + "ARCEEAI_API_KEY": _env("Arcee AI API key", "Arcee AI API key", url="https://chat.arcee.ai/", + password=True, category="provider", advanced=True), + "ARCEE_BASE_URL": _env("Arcee AI base URL override", "Arcee base URL (leave empty for default)", url=None, + password=False, category="provider", advanced=True), + "GMI_API_KEY": _env("GMI Cloud API key", "GMI Cloud API key", url="https://www.gmicloud.ai/", + password=True, category="provider", advanced=True), + "GMI_BASE_URL": _env("GMI Cloud base URL override", "GMI Cloud base URL (leave empty for default)", + url=None, password=False, category="provider", advanced=True), + "ACTUAL_API_KEY": _env("Actual Computer inference key (ac_...)", "Actual Computer inference key", + url="https://actual.inc/user/keys", password=True, category="provider", advanced=True), + "ACTUAL_BASE_URL": _env( + "Actual Computer base URL override (set to http://127.0.0.1:8080 for the local offline daemon)", + "Actual Computer base URL (leave empty for hosted relay)", url=None, password=False, + category="provider", advanced=True), + "FIREWORKS_API_KEY": _env("Fireworks AI API key", "Fireworks AI API key", + url="https://app.fireworks.ai/settings/users/api-keys", password=True, category="provider", + advanced=True), + "MINIMAX_API_KEY": _env("MiniMax API key (international)", "MiniMax API key", + url="https://www.minimax.io/", password=True, category="provider", advanced=True), + "MINIMAX_BASE_URL": _env("MiniMax base URL override", "MiniMax base URL (leave empty for default)", + url=None, password=False, category="provider", advanced=True), + "MINIMAX_CN_API_KEY": _env("MiniMax API key (China endpoint)", "MiniMax (China) API key", + url="https://www.minimaxi.com/", password=True, category="provider", advanced=True), + "MINIMAX_CN_BASE_URL": _env("MiniMax (China) base URL override", + "MiniMax (China) base URL (leave empty for default)", url=None, password=False, category="provider", + advanced=True), + "DEEPSEEK_API_KEY": _env("DeepSeek API key for direct DeepSeek access", "DeepSeek API Key", + url="https://platform.deepseek.com/api_keys", password=True, category="provider"), + "DEEPSEEK_BASE_URL": _env("Custom DeepSeek API base URL (advanced)", "DeepSeek Base URL", url="", + password=False, category="provider"), + "DASHSCOPE_API_KEY": _env("Alibaba Cloud DashScope API key (Qwen + multi-provider models)", + "DashScope API Key", url="https://modelstudio.console.alibabacloud.com/", password=True, + category="provider"), + "DASHSCOPE_BASE_URL": _env("Custom DashScope base URL (default: coding-intl OpenAI-compat endpoint)", + "DashScope Base URL", url="", password=False, category="provider", advanced=True), + "HERMES_QWEN_BASE_URL": _env("Qwen Portal base URL override (default: https://portal.qwen.ai/v1)", + "Qwen Portal base URL (leave empty for default)", url=None, password=False, category="provider", + advanced=True), + "OPENCODE_ZEN_API_KEY": _env("OpenCode Zen API key (pay-as-you-go access to curated models)", + "OpenCode Zen API key", url="https://opencode.ai/auth", password=True, category="provider", + advanced=True), + "COMMANDCODE_API_KEY": _env("CommandCode API key (GOAT/Pro/Max/Provider plans — 30+ models via one key)", + "CommandCode API key", url="https://commandcode.ai/studio/", password=True, category="provider", + advanced=True), + "OPENCODE_ZEN_BASE_URL": _env("OpenCode Zen base URL override", + "OpenCode Zen base URL (leave empty for default)", url=None, password=False, category="provider", + advanced=True), + "OPENCODE_GO_API_KEY": _env("OpenCode Go API key ($10/month subscription for open models)", + "OpenCode Go API key", url="https://opencode.ai/auth", password=True, category="provider", + advanced=True), + "OPENCODE_GO_BASE_URL": _env("OpenCode Go base URL override", + "OpenCode Go base URL (leave empty for default)", url=None, password=False, category="provider", + advanced=True), + "HF_TOKEN": _env("Hugging Face token for Inference Providers (20+ open models via router.huggingface.co)", + "Hugging Face Token", url="https://huggingface.co/settings/tokens", password=True, + category="provider"), + "HF_BASE_URL": _env("Hugging Face Inference Providers base URL override", + "HF base URL (leave empty for default)", url=None, password=False, category="provider", + advanced=True), + "OLLAMA_API_KEY": _env("Ollama Cloud API key (ollama.com — cloud-hosted open models)", + "Ollama Cloud API key", url="https://ollama.com/settings", password=True, category="provider", + advanced=True), + "OLLAMA_BASE_URL": _env("Ollama Cloud base URL override (default: https://ollama.com/v1)", + "Ollama base URL (leave empty for default)", url=None, password=False, category="provider", + advanced=True), + "XIAOMI_API_KEY": _env( + "Xiaomi MiMo API key for MiMo models (mimo-v2.5-pro, mimo-v2.5, mimo-v2-pro, mimo-v2-omni, " + "mimo-v2-flash)", "Xiaomi MiMo API Key", url="https://platform.xiaomimimo.com", password=True, + category="provider"), + "XIAOMI_BASE_URL": _env("Xiaomi MiMo base URL override (default: https://api.xiaomimimo.com/v1)", + "Xiaomi base URL (leave empty for default)", url=None, password=False, category="provider", + advanced=True), + "UPSTAGE_API_KEY": _env("Upstage API key for Solar LLM models", "Upstage API Key", + url="https://console.upstage.ai/api-keys", password=True, category="provider"), + "UPSTAGE_BASE_URL": _env("Upstage base URL override (default: https://api.upstage.ai/v1)", + "Upstage base URL (leave empty for default)", url=None, password=False, category="provider", + advanced=True), + "AWS_REGION": _env("AWS region for Bedrock API calls (e.g. us-east-1, eu-central-1)", "AWS Region", + url="https://docs.aws.amazon.com/bedrock/latest/userguide/bedrock-regions.html", password=False, + category="provider", advanced=True), + "AWS_PROFILE": _env("AWS named profile for Bedrock authentication (from ~/.aws/credentials)", + "AWS Profile", url=None, password=False, category="provider", advanced=True), + "AZURE_FOUNDRY_API_KEY": _env("Azure Foundry API key for custom Azure endpoints", "Azure Foundry API Key", + url="https://ai.azure.com/", password=True, category="provider"), + "AZURE_FOUNDRY_BASE_URL": _env( + "Azure Foundry base URL (set via 'hermes model' for endpoint-specific config)", + "Azure Foundry base URL", url=None, password=False, category="provider", advanced=True), # ── Tool API keys ── - "EXA_API_KEY": { - "description": "Exa API key for AI-native web search and contents", - "prompt": "Exa API key", - "url": "https://exa.ai/", - "tools": ["web_search", "web_extract"], - "password": True, - "category": "tool", - }, - "PARALLEL_API_KEY": { - "description": "Parallel API key for AI-native web search and extract", - "prompt": "Parallel API key", - "url": "https://parallel.ai/", - "tools": ["web_search", "web_extract"], - "password": True, - "category": "tool", - }, - "FIRECRAWL_API_KEY": { - "description": "Firecrawl API key for web search and scraping", - "prompt": "Firecrawl API key", - "url": "https://firecrawl.dev/", - "tools": ["web_search", "web_extract"], - "password": True, - "category": "tool", - }, - "FIRECRAWL_API_URL": { - "description": "Firecrawl API URL for self-hosted instances (optional)", - "prompt": "Firecrawl API URL (leave empty for cloud)", - "url": None, - "password": False, - "category": "tool", - "advanced": True, - }, - "FIRECRAWL_GATEWAY_URL": { - "description": "Exact Firecrawl tool-gateway origin override for Nous Subscribers only (optional)", - "prompt": "Firecrawl gateway URL (leave empty to derive from domain)", - "url": None, - "password": False, - "category": "tool", - "advanced": True, - }, - "TOOL_GATEWAY_DOMAIN": { - "description": "Shared tool-gateway domain suffix for Nous Subscribers only, used to derive vendor hosts, e.g. nousresearch.com -> firecrawl-gateway.nousresearch.com", - "prompt": "Tool-gateway domain suffix", - "url": None, - "password": False, - "category": "tool", - "advanced": True, - }, - "TOOL_GATEWAY_SCHEME": { - "description": "Shared tool-gateway URL scheme for Nous Subscribers only, used to derive vendor hosts (`https` by default, set `http` for local gateway testing)", - "prompt": "Tool-gateway URL scheme", - "url": None, - "password": False, - "category": "tool", - "advanced": True, - }, - "TOOL_GATEWAY_USER_TOKEN": { - "description": "Explicit Nous Subscriber access token for tool-gateway requests (optional; otherwise read from the Hermes auth store)", - "prompt": "Tool-gateway user token", - "url": None, - "password": True, - "category": "tool", - "advanced": True, - }, - "TAVILY_API_KEY": { - "description": "Tavily API key for AI-native web search and extract (optional — keyless works when Tavily is selected)", - "prompt": "Tavily API key", - "url": "https://app.tavily.com/home", - "tools": ["web_search", "web_extract"], - "password": True, - "category": "tool", - }, - "KEENABLE_API_KEY": { - "description": "Keenable API key for fast independent-index web search and page fetch (optional — keyless free tier works without it)", - "prompt": "Keenable API key", - "url": "https://keenable.ai", - "tools": ["web_search", "web_extract"], - "password": True, - "category": "tool", - }, - "SEARXNG_URL": { - "description": "URL of your SearXNG instance for free self-hosted web search", - "prompt": "SearXNG URL (e.g. http://localhost:8080)", - "url": "https://searxng.github.io/searxng/", - "tools": ["web_search"], - "password": False, - "category": "tool", - }, - "BRAVE_SEARCH_API_KEY": { - "description": "Brave Search API subscription token (free tier: 2,000 queries/mo)", - "prompt": "Brave Search subscription token", - "url": "https://brave.com/search/api/", - "tools": ["web_search"], - "password": True, - "category": "tool", - }, - "BROWSERBASE_API_KEY": { - "description": "Browserbase API key for cloud browser (optional — local browser works without this)", - "prompt": "Browserbase API key", - "url": "https://browserbase.com/", - "tools": ["browser_navigate", "browser_click"], - "password": True, - "category": "tool", - }, - "BROWSERBASE_PROJECT_ID": { - "description": "Browserbase project ID (optional — only needed for cloud browser)", - "prompt": "Browserbase project ID", - "url": "https://browserbase.com/", - "tools": ["browser_navigate", "browser_click"], - "password": False, - "category": "tool", - }, - "BROWSER_USE_API_KEY": { - "description": "Browser Use API key for cloud browser (optional — local browser works without this)", - "prompt": "Browser Use API key", - "url": "https://browser-use.com/", - "tools": ["browser_navigate", "browser_click"], - "password": True, - "category": "tool", - }, - "FIRECRAWL_BROWSER_TTL": { - "description": "Firecrawl browser session TTL in seconds (optional, default 300)", - "prompt": "Browser session TTL (seconds)", - "tools": ["browser_navigate", "browser_click"], - "password": False, - "category": "tool", - }, - "AGENT_BROWSER_ENGINE": { - "description": "Local browser engine: auto (default Chrome), lightpanda (faster, no screenshots; Browser Use mode spawns lightpanda serve), chrome", - "prompt": "Browser engine (auto/lightpanda/chrome)", - "url": "https://lightpanda.io/docs/run-locally/installation/one-liner", - "tools": ["browser_exec", "browser_navigate", "browser_snapshot", "browser_click", "browser_vision"], - "password": False, - "category": "tool", - "advanced": True, - }, - "CAMOFOX_URL": { - "description": "Camofox browser server URL for local anti-detection browsing (e.g. http://localhost:9377)", - "prompt": "Camofox server URL", - "url": "https://github.com/jo-inc/camofox-browser", - "tools": ["browser_navigate", "browser_click"], - "password": False, - "category": "tool", - }, - "CAMOFOX_API_KEY": { - "description": "Optional bearer token sent as Authorization header to a remote/authenticated Camofox server", - "prompt": "Camofox API key", - "url": "https://github.com/jo-inc/camofox-browser", - "tools": ["browser_navigate", "browser_click"], - "password": True, - "category": "tool", - "advanced": True, - }, - "FAL_KEY": { - "description": "FAL API key for image and video generation", - "prompt": "FAL API key", - "url": "https://fal.ai/", - "tools": ["image_generate", "video_generate"], - "password": True, - "category": "tool", - }, - "KREA_API_KEY": { - "description": "Krea API key for Krea 2 image generation (Medium + Large)", - "prompt": "Krea API key", - "url": "https://www.krea.ai/settings/api-tokens", - "tools": ["image_generate"], - "password": True, - "category": "tool", - }, - "VOICE_TOOLS_OPENAI_KEY": { - "description": "OpenAI API key for voice transcription (Whisper) and OpenAI TTS", - "prompt": "OpenAI API Key (for Whisper STT + TTS)", - "url": "https://platform.openai.com/api-keys", - "tools": ["voice_transcription", "openai_tts"], - "password": True, - "category": "tool", - }, - "ELEVENLABS_API_KEY": { - "description": "ElevenLabs API key for premium text-to-speech voices and Scribe transcription", - "prompt": "ElevenLabs API key", - "url": "https://elevenlabs.io/", - "tools": ["elevenlabs_tts", "voice_transcription"], - "password": True, - "category": "tool", - }, - "MISTRAL_API_KEY": { - "description": "Mistral API key for Voxtral TTS and transcription (STT)", - "prompt": "Mistral API key", - "url": "https://console.mistral.ai/", - "password": True, - "category": "tool", - }, - "PORCUPINE_ACCESS_KEY": { - "description": "Picovoice access key for the Porcupine 'Hey Hermes' wake word engine (optional; openWakeWord is the free default)", - "prompt": "Picovoice access key", - "url": "https://console.picovoice.ai/", - "password": True, - "category": "tool", - }, - "GITHUB_TOKEN": { - "description": "GitHub token for Skills Hub (higher API rate limits, skill publish)", - "prompt": "GitHub Token", - "url": "https://github.com/settings/tokens", - "password": True, - "category": "tool", - }, + "EXA_API_KEY": _env("Exa API key for AI-native web search and contents", "Exa API key", + url="https://exa.ai/", tools=["web_search", "web_extract"], password=True, category="tool"), + "PARALLEL_API_KEY": _env("Parallel API key for AI-native web search and extract", "Parallel API key", + url="https://parallel.ai/", tools=["web_search", "web_extract"], password=True, category="tool"), + "FIRECRAWL_API_KEY": _env("Firecrawl API key for web search and scraping", "Firecrawl API key", + url="https://firecrawl.dev/", tools=["web_search", "web_extract"], password=True, category="tool"), + "FIRECRAWL_API_URL": _env("Firecrawl API URL for self-hosted instances (optional)", + "Firecrawl API URL (leave empty for cloud)", url=None, password=False, category="tool", + advanced=True), + "FIRECRAWL_GATEWAY_URL": _env( + "Exact Firecrawl tool-gateway origin override for Nous Subscribers only (optional)", + "Firecrawl gateway URL (leave empty to derive from domain)", url=None, password=False, + category="tool", advanced=True), + "TOOL_GATEWAY_DOMAIN": _env( + "Shared tool-gateway domain suffix for Nous Subscribers only, used to derive vendor hosts, e.g. " + "nousresearch.com -> firecrawl-gateway.nousresearch.com", "Tool-gateway domain suffix", url=None, + password=False, category="tool", advanced=True), + "TOOL_GATEWAY_SCHEME": _env( + "Shared tool-gateway URL scheme for Nous Subscribers only, used to derive vendor hosts (`https` by " + "default, set `http` for local gateway testing)", "Tool-gateway URL scheme", url=None, password=False, + category="tool", advanced=True), + "TOOL_GATEWAY_USER_TOKEN": _env( + "Explicit Nous Subscriber access token for tool-gateway requests (optional; otherwise read from " + "the Hermes auth store)", "Tool-gateway user token", url=None, password=True, category="tool", + advanced=True), + "TAVILY_API_KEY": _env( + "Tavily API key for AI-native web search and extract (optional — keyless works when Tavily is " + "selected)", "Tavily API key", url="https://app.tavily.com/home", tools=["web_search", "web_extract"], + password=True, category="tool"), + "KEENABLE_API_KEY": _env( + "Keenable API key for fast independent-index web search and page fetch (optional — keyless free " + "tier works without it)", "Keenable API key", url="https://keenable.ai", + tools=["web_search", "web_extract"], password=True, category="tool"), + "SEARXNG_URL": _env("URL of your SearXNG instance for free self-hosted web search", + "SearXNG URL (e.g. http://localhost:8080)", url="https://searxng.github.io/searxng/", + tools=["web_search"], password=False, category="tool"), + "BRAVE_SEARCH_API_KEY": _env("Brave Search API subscription token (free tier: 2,000 queries/mo)", + "Brave Search subscription token", url="https://brave.com/search/api/", tools=["web_search"], + password=True, category="tool"), + "BROWSERBASE_API_KEY": _env( + "Browserbase API key for cloud browser (optional — local browser works without this)", + "Browserbase API key", url="https://browserbase.com/", tools=["browser_navigate", "browser_click"], + password=True, category="tool"), + "BROWSERBASE_PROJECT_ID": _env("Browserbase project ID (optional — only needed for cloud browser)", + "Browserbase project ID", url="https://browserbase.com/", tools=["browser_navigate", "browser_click"], + password=False, category="tool"), + "BROWSER_USE_API_KEY": _env( + "Browser Use API key for cloud browser (optional — local browser works without this)", + "Browser Use API key", url="https://browser-use.com/", tools=["browser_navigate", "browser_click"], + password=True, category="tool"), + "FIRECRAWL_BROWSER_TTL": _env("Firecrawl browser session TTL in seconds (optional, default 300)", + "Browser session TTL (seconds)", tools=["browser_navigate", "browser_click"], password=False, + category="tool"), + "AGENT_BROWSER_ENGINE": _env( + "Local browser engine: auto (default Chrome), lightpanda (faster, no screenshots; Browser Use mode " + "spawns lightpanda serve), chrome", "Browser engine (auto/lightpanda/chrome)", + url="https://lightpanda.io/docs/run-locally/installation/one-liner", + tools=["browser_exec", "browser_navigate", "browser_snapshot", "browser_click", "browser_vision"], + password=False, category="tool", advanced=True), + "CAMOFOX_URL": _env( + "Camofox browser server URL for local anti-detection browsing (e.g. http://localhost:9377)", + "Camofox server URL", url="https://github.com/jo-inc/camofox-browser", + tools=["browser_navigate", "browser_click"], password=False, category="tool"), + "CAMOFOX_API_KEY": _env( + "Optional bearer token sent as Authorization header to a remote/authenticated Camofox server", + "Camofox API key", url="https://github.com/jo-inc/camofox-browser", + tools=["browser_navigate", "browser_click"], password=True, category="tool", advanced=True), + "FAL_KEY": _env("FAL API key for image and video generation", "FAL API key", url="https://fal.ai/", + tools=["image_generate", "video_generate"], password=True, category="tool"), + "KREA_API_KEY": _env("Krea API key for Krea 2 image generation (Medium + Large)", "Krea API key", + url="https://www.krea.ai/settings/api-tokens", tools=["image_generate"], password=True, + category="tool"), + "VOICE_TOOLS_OPENAI_KEY": _env("OpenAI API key for voice transcription (Whisper) and OpenAI TTS", + "OpenAI API Key (for Whisper STT + TTS)", url="https://platform.openai.com/api-keys", + tools=["voice_transcription", "openai_tts"], password=True, category="tool"), + "ELEVENLABS_API_KEY": _env( + "ElevenLabs API key for premium text-to-speech voices and Scribe transcription", "ElevenLabs API key", + url="https://elevenlabs.io/", tools=["elevenlabs_tts", "voice_transcription"], password=True, + category="tool"), + "MISTRAL_API_KEY": _env("Mistral API key for Voxtral TTS and transcription (STT)", "Mistral API key", + url="https://console.mistral.ai/", password=True, category="tool"), + "PORCUPINE_ACCESS_KEY": _env( + "Picovoice access key for the Porcupine 'Hey Hermes' wake word engine (optional; openWakeWord is " + "the free default)", "Picovoice access key", url="https://console.picovoice.ai/", password=True, + category="tool"), + "GITHUB_TOKEN": _env("GitHub token for Skills Hub (higher API rate limits, skill publish)", + "GitHub Token", url="https://github.com/settings/tokens", password=True, category="tool"), # ── Bundled skills (opt-in: only needed if the user uses that skill) ── # These use category="skill" (distinct from "tool") so the sandbox # env blocklist in tools/environments/local.py does NOT rewrite them — # skills legitimately need these passed through to curl via # tools/env_passthrough.py when the user's skill calls out. - "NOTION_API_KEY": { - "description": "Notion integration token (used by the `notion` skill)", - "prompt": "Notion API key", - "url": "https://www.notion.so/my-integrations", - "password": True, - "category": "skill", - "advanced": True, - }, - "LINEAR_API_KEY": { - "description": "Linear personal API key (used by the `linear` skill)", - "prompt": "Linear API key", - "url": "https://linear.app/settings/account/security", - "password": True, - "category": "skill", - "advanced": True, - }, - "AIRTABLE_API_KEY": { - "description": "Airtable personal access token (used by the `airtable` skill)", - "prompt": "Airtable API key", - "url": "https://airtable.com/create/tokens", - "password": True, - "category": "skill", - "advanced": True, - }, - "TENOR_API_KEY": { - "description": "Tenor API key for GIF search (used by the `gif-search` skill)", - "prompt": "Tenor API key", - "url": "https://developers.google.com/tenor/guides/quickstart", - "password": True, - "category": "skill", - "advanced": True, - }, + "NOTION_API_KEY": _env("Notion integration token (used by the `notion` skill)", "Notion API key", + url="https://www.notion.so/my-integrations", password=True, category="skill", advanced=True), + "LINEAR_API_KEY": _env("Linear personal API key (used by the `linear` skill)", "Linear API key", + url="https://linear.app/settings/account/security", password=True, category="skill", advanced=True), + "AIRTABLE_API_KEY": _env("Airtable personal access token (used by the `airtable` skill)", + "Airtable API key", url="https://airtable.com/create/tokens", password=True, category="skill", + advanced=True), + "TENOR_API_KEY": _env("Tenor API key for GIF search (used by the `gif-search` skill)", "Tenor API key", + url="https://developers.google.com/tenor/guides/quickstart", password=True, category="skill", + advanced=True), # ── Honcho ── - "HONCHO_API_KEY": { - "description": "Honcho API key for AI-native persistent memory", - "prompt": "Honcho API key", - "url": "https://app.honcho.dev", - "tools": ["honcho_context"], - "password": True, - "category": "tool", - }, - "HONCHO_BASE_URL": { - "description": "Base URL for self-hosted Honcho instances (no API key needed)", - "prompt": "Honcho base URL (e.g. http://localhost:8000)", - "category": "tool", - }, + "HONCHO_API_KEY": _env("Honcho API key for AI-native persistent memory", "Honcho API key", + url="https://app.honcho.dev", tools=["honcho_context"], password=True, category="tool"), + "HONCHO_BASE_URL": _env("Base URL for self-hosted Honcho instances (no API key needed)", + "Honcho base URL (e.g. http://localhost:8000)", category="tool"), # ── Hindsight ── - "HINDSIGHT_API_KEY": { - "description": "Hindsight API key for graph-aware persistent memory", - "prompt": "Hindsight API key", - "url": "https://hindsight.vectorize.io", - "tools": ["hindsight_recall"], - "password": True, - "category": "tool", - }, - "HINDSIGHT_API_URL": { - "description": "Base URL for the Hindsight API (default: https://api.hindsight.vectorize.io)", - "prompt": "Hindsight API URL", - "category": "tool", - "advanced": True, - }, + "HINDSIGHT_API_KEY": _env("Hindsight API key for graph-aware persistent memory", "Hindsight API key", + url="https://hindsight.vectorize.io", tools=["hindsight_recall"], password=True, category="tool"), + "HINDSIGHT_API_URL": _env("Base URL for the Hindsight API (default: https://api.hindsight.vectorize.io)", + "Hindsight API URL", category="tool", advanced=True), # ── Supermemory ── - "SUPERMEMORY_API_KEY": { - "description": "Supermemory API key for conversation-scoped persistent memory", - "prompt": "Supermemory API key", - "url": "https://supermemory.ai", - "tools": ["supermemory_search"], - "password": True, - "category": "tool", - }, + "SUPERMEMORY_API_KEY": _env("Supermemory API key for conversation-scoped persistent memory", + "Supermemory API key", url="https://supermemory.ai", tools=["supermemory_search"], password=True, + category="tool"), # ── Mem0 ── - "MEM0_API_KEY": { - "description": "Mem0 Platform API key for semantic persistent memory", - "prompt": "Mem0 API key", - "url": "https://app.mem0.ai", - "tools": ["mem0_search"], - "password": True, - "category": "tool", - }, + "MEM0_API_KEY": _env("Mem0 Platform API key for semantic persistent memory", "Mem0 API key", + url="https://app.mem0.ai", tools=["mem0_search"], password=True, category="tool"), # ── RetainDB ── - "RETAINDB_API_KEY": { - "description": "RetainDB API key for persistent memory", - "prompt": "RetainDB API key", - "url": "https://retaindb.com", - "tools": ["retaindb_search"], - "password": True, - "category": "tool", - }, - "RETAINDB_BASE_URL": { - "description": "Base URL for self-hosted RetainDB instances (default: https://api.retaindb.com)", - "prompt": "RetainDB base URL", - "category": "tool", - "advanced": True, - }, + "RETAINDB_API_KEY": _env("RetainDB API key for persistent memory", "RetainDB API key", + url="https://retaindb.com", tools=["retaindb_search"], password=True, category="tool"), + "RETAINDB_BASE_URL": _env( + "Base URL for self-hosted RetainDB instances (default: https://api.retaindb.com)", + "RetainDB base URL", category="tool", advanced=True), # ── ByteRover ── - "BRV_API_KEY": { - "description": "ByteRover API key (optional, for cloud sync — local-first by default)", - "prompt": "ByteRover API key", - "url": "https://app.byterover.dev", - "tools": ["brv_query"], - "password": True, - "category": "tool", - }, + "BRV_API_KEY": _env("ByteRover API key (optional, for cloud sync — local-first by default)", + "ByteRover API key", url="https://app.byterover.dev", tools=["brv_query"], password=True, + category="tool"), # ── OpenViking ── - "OPENVIKING_API_KEY": { - "description": "OpenViking API key (leave blank for local dev mode)", - "prompt": "OpenViking API key", - "tools": ["viking_search"], - "password": True, - "category": "tool", - }, - "OPENVIKING_ENDPOINT": { - "description": "OpenViking server URL (default: http://127.0.0.1:1933)", - "prompt": "OpenViking endpoint", - "category": "tool", - "advanced": True, - }, + "OPENVIKING_API_KEY": _env("OpenViking API key (leave blank for local dev mode)", "OpenViking API key", + tools=["viking_search"], password=True, category="tool"), + "OPENVIKING_ENDPOINT": _env("OpenViking server URL (default: http://127.0.0.1:1933)", + "OpenViking endpoint", category="tool", advanced=True), # ── Langfuse observability ── - "HERMES_LANGFUSE_PUBLIC_KEY": { - "description": "Langfuse project public key (pk-lf-...)", - "prompt": "Langfuse public key", - "url": "https://cloud.langfuse.com", - "password": False, - "category": "tool", - }, - "HERMES_LANGFUSE_SECRET_KEY": { - "description": "Langfuse project secret key (sk-lf-...)", - "prompt": "Langfuse secret key", - "url": "https://cloud.langfuse.com", - "password": True, - "category": "tool", - }, - "HERMES_LANGFUSE_BASE_URL": { - "description": "Langfuse server URL (default: https://cloud.langfuse.com)", - "prompt": "Langfuse server URL (leave empty for cloud.langfuse.com)", - "url": None, - "password": False, - "category": "tool", - "advanced": True, - }, + "HERMES_LANGFUSE_PUBLIC_KEY": _env("Langfuse project public key (pk-lf-...)", "Langfuse public key", + url="https://cloud.langfuse.com", password=False, category="tool"), + "HERMES_LANGFUSE_SECRET_KEY": _env("Langfuse project secret key (sk-lf-...)", "Langfuse secret key", + url="https://cloud.langfuse.com", password=True, category="tool"), + "HERMES_LANGFUSE_BASE_URL": _env("Langfuse server URL (default: https://cloud.langfuse.com)", + "Langfuse server URL (leave empty for cloud.langfuse.com)", url=None, password=False, category="tool", + advanced=True), # ── Messaging platforms ── - "TELEGRAM_BOT_TOKEN": { - "description": "Complete Telegram bot token created by @BotFather (numeric bot ID followed by a colon and secret)", - "prompt": "Telegram bot token", - "url": "https://t.me/BotFather", - "password": True, - "category": "messaging", - }, - "TELEGRAM_ALLOWED_USERS": { - "description": "Optional comma-separated numeric Telegram user IDs allowed immediately; leave blank to approve new users through DM pairing", - "prompt": "Allowed Telegram user IDs (comma-separated)", - "url": "https://t.me/userinfobot", - "password": False, - "category": "messaging", - }, - "TELEGRAM_PROXY": { - "description": "Proxy URL for Telegram connections (overrides HTTPS_PROXY). Supports http://, https://, socks5://", - "prompt": "Telegram proxy URL (optional)", - "password": False, - "category": "messaging", - }, - "DISCORD_BOT_TOKEN": { - "description": "Discord bot token from Developer Portal", - "prompt": "Discord bot token", - "url": "https://discord.com/developers/applications", - "password": True, - "category": "messaging", - }, - "DISCORD_ALLOWED_USERS": { - "description": "Comma-separated Discord user IDs allowed to use the bot", - "prompt": "Allowed Discord user IDs (comma-separated)", - "url": None, - "password": False, - "category": "messaging", - }, - "DISCORD_REPLY_TO_MODE": { - "description": "Discord reply threading mode: 'off' (no reply references), 'first' (reply on first message only, default), 'all' (reply on every chunk)", - "prompt": "Discord reply mode (off/first/all)", - "url": None, - "password": False, - "category": "messaging", - }, - "SLACK_BOT_TOKEN": { - "description": "Slack bot token (xoxb-). Get from OAuth & Permissions after installing your app. " - "Required scopes: chat:write, app_mentions:read, channels:history, groups:history, " - "im:history, im:read, im:write, mpim:history, mpim:read, users:read, files:read, files:write", - "prompt": "Slack Bot Token (xoxb-...)", - "help": "In your Slack app, add the required bot scopes, install the app to the workspace, then copy OAuth & Permissions > Bot User OAuth Token.", - "url": "https://api.slack.com/apps", - "password": True, - "category": "messaging", - }, - "SLACK_APP_TOKEN": { - "description": "Slack app-level token (xapp-) for Socket Mode. Get from Basic Information → " - "App-Level Tokens. Also ensure Event Subscriptions include: message.im, " - "message.channels, message.groups, message.mpim, app_mention", - "prompt": "Slack App Token (xapp-...)", - "help": "In your Slack app, enable Socket Mode, then create Basic Information > App-Level Tokens with the connections:write scope.", - "url": "https://api.slack.com/apps", - "password": True, - "category": "messaging", - }, - "SLACK_ALLOWED_USERS": { - "description": "Comma-separated Slack member IDs allowed to use Hermes, e.g. U01ABC2DEF3. Without this, Slack may connect but deny messages by default.", - "prompt": "Allowed Slack member IDs", - "help": "In Slack, open your profile, choose More or the three-dot menu, then Copy member ID. Add multiple IDs comma-separated.", - "url": "https://api.slack.com/apps", - "password": False, - "category": "messaging", - }, - "MATTERMOST_URL": { - "description": "Mattermost server URL (e.g. https://mm.example.com)", - "prompt": "Mattermost server URL", - "url": "https://mattermost.com/deploy/", - "password": False, - "category": "messaging", - }, - "MATTERMOST_TOKEN": { - "description": "Mattermost bot token or personal access token", - "prompt": "Mattermost bot token", - "url": None, - "password": True, - "category": "messaging", - }, - "MATTERMOST_ALLOWED_USERS": { - "description": "Comma-separated Mattermost user IDs allowed to use the bot", - "prompt": "Allowed Mattermost user IDs (comma-separated)", - "url": None, - "password": False, - "category": "messaging", - }, - "MATTERMOST_REQUIRE_MENTION": { - "description": "Require @mention in Mattermost channels (default: true). Set to false to respond to all messages.", - "prompt": "Require @mention in channels", - "url": None, - "password": False, - "category": "messaging", - }, - "MATTERMOST_FREE_RESPONSE_CHANNELS": { - "description": "Comma-separated Mattermost channel IDs where bot responds without @mention", - "prompt": "Free-response channel IDs (comma-separated)", - "url": None, - "password": False, - "category": "messaging", - }, - "MATRIX_HOMESERVER": { - "description": "Matrix homeserver URL (e.g. https://matrix.example.org)", - "prompt": "Matrix homeserver URL", - "url": "https://matrix.org/ecosystem/servers/", - "password": False, - "category": "messaging", - }, - "MATRIX_ACCESS_TOKEN": { - "description": "Matrix access token (preferred over password login)", - "prompt": "Matrix access token", - "url": None, - "password": True, - "category": "messaging", - }, - "MATRIX_USER_ID": { - "description": "Matrix user ID (e.g. @hermes:example.org)", - "prompt": "Matrix user ID (@user:server)", - "url": None, - "password": False, - "category": "messaging", - }, - "MATRIX_ALLOWED_USERS": { - "description": "Comma-separated Matrix user IDs allowed to use the bot (@user:server format)", - "prompt": "Allowed Matrix user IDs (comma-separated)", - "url": None, - "password": False, - "category": "messaging", - }, - "MATRIX_REQUIRE_MENTION": { - "description": "Require @mention in Matrix rooms (default: true). Set to false to respond to all messages.", - "prompt": "Require @mention in rooms (true/false)", - "url": None, - "password": False, - "category": "messaging", - "advanced": True, - }, - "MATRIX_FREE_RESPONSE_ROOMS": { - "description": "Comma-separated Matrix room IDs where bot responds without @mention", - "prompt": "Free-response room IDs (comma-separated)", - "url": None, - "password": False, - "category": "messaging", - "advanced": True, - }, - "MATRIX_AUTO_THREAD": { - "description": "Auto-create threads for messages in Matrix rooms (default: true)", - "prompt": "Auto-create threads in rooms (true/false)", - "url": None, - "password": False, - "category": "messaging", - "advanced": True, - }, - "MATRIX_DM_AUTO_THREAD": { - "description": "Auto-create threads for DM messages in Matrix (default: false)", - "prompt": "Auto-create threads in DMs (true/false)", - "url": None, - "password": False, - "category": "messaging", - "advanced": True, - }, - "MATRIX_DEVICE_ID": { - "description": "Stable Matrix device ID for E2EE persistence across restarts (e.g. HERMES_BOT)", - "prompt": "Matrix device ID (stable across restarts)", - "url": None, - "password": False, - "category": "messaging", - "advanced": True, - }, - "MATRIX_RECOVERY_KEY": { - "description": "Matrix recovery key for cross-signing verification after device key rotation (from Element: Settings → Security → Recovery Key)", - "prompt": "Matrix recovery key", - "url": None, - "password": True, - "category": "messaging", - "advanced": True, - }, - "BLUEBUBBLES_SERVER_URL": { - "description": "BlueBubbles server URL for iMessage integration (e.g. http://192.168.1.10:1234)", - "prompt": "BlueBubbles server URL", - "url": "https://bluebubbles.app/", - "password": False, - "category": "messaging", - }, - "BLUEBUBBLES_PASSWORD": { - "description": "BlueBubbles server password (from BlueBubbles Server → Settings → API)", - "prompt": "BlueBubbles server password", - "url": None, - "password": True, - "category": "messaging", - }, - "BLUEBUBBLES_ALLOWED_USERS": { - "description": "Comma-separated iMessage addresses (email or phone) allowed to use the bot", - "prompt": "Allowed iMessage addresses (comma-separated)", - "url": None, - "password": False, - "category": "messaging", - }, - "BLUEBUBBLES_ALLOW_ALL_USERS": { - "description": "Allow all BlueBubbles users without allowlist", - "prompt": "Allow All BlueBubbles Users", - "category": "messaging", - }, - "QQ_APP_ID": { - "description": "QQ Bot App ID from QQ Open Platform (q.qq.com)", - "prompt": "QQ App ID", - "url": "https://q.qq.com", - "category": "messaging", - }, - "QQ_CLIENT_SECRET": { - "description": "QQ Bot Client Secret from QQ Open Platform", - "prompt": "QQ Client Secret", - "password": True, - "category": "messaging", - }, - "QQ_ALLOWED_USERS": { - "description": "Comma-separated QQ user IDs allowed to use the bot", - "prompt": "QQ Allowed Users", - "category": "messaging", - }, - "QQ_GROUP_ALLOWED_USERS": { - "description": "Comma-separated QQ group IDs allowed to interact with the bot", - "prompt": "QQ Group Allowed Users", - "category": "messaging", - }, - "QQ_ALLOW_ALL_USERS": { - "description": "Allow all QQ users without an allowlist (true/false)", - "prompt": "Allow All QQ Users", - "category": "messaging", - }, - "QQBOT_HOME_CHANNEL": { - "description": "Default QQ channel/group for cron delivery and notifications", - "prompt": "QQ Home Channel", - "category": "messaging", - }, - "QQBOT_HOME_CHANNEL_NAME": { - "description": "Display name for the QQ home channel", - "prompt": "QQ Home Channel Name", - "category": "messaging", - }, - "QQ_SANDBOX": { - "description": "Enable QQ sandbox mode for development testing (true/false)", - "prompt": "QQ Sandbox Mode", - "category": "messaging", - }, - "IRC_SERVER": { - "description": "IRC server hostname (e.g. irc.libera.chat)", - "prompt": "IRC server", - "url": None, - "password": False, - "category": "messaging", - }, - "IRC_CHANNEL": { - "description": "IRC channel to join (e.g. #hermes)", - "prompt": "IRC channel", - "url": None, - "password": False, - "category": "messaging", - }, - "IRC_NICKNAME": { - "description": "Bot nickname on IRC (default: hermes-bot)", - "prompt": "IRC nickname", - "url": None, - "password": False, - "category": "messaging", - }, - "IRC_SERVER_PASSWORD": { - "description": "IRC server password (if required)", - "prompt": "IRC server password", - "url": None, - "password": True, - "category": "messaging", - "advanced": True, - }, - "IRC_NICKSERV_PASSWORD": { - "description": "NickServ password for nick identification", - "prompt": "NickServ password", - "url": None, - "password": True, - "category": "messaging", - "advanced": True, - }, - "GATEWAY_ALLOW_ALL_USERS": { - "description": "Allow all users to interact with messaging bots (true/false). Default: false.", - "prompt": "Allow all users (true/false)", - "url": None, - "password": False, - "category": "messaging", - "advanced": True, - }, - "API_SERVER_ENABLED": { - "description": "Enable the OpenAI-compatible API server (true/false). Allows frontends like Open WebUI, LobeChat, etc. to connect.", - "prompt": "Enable API server (true/false)", - "url": None, - "password": False, - "category": "messaging", - "advanced": True, - }, - "API_SERVER_KEY": { - "description": "Bearer token for API server authentication. Required whenever the API server is enabled; server refuses to start without it.", - "prompt": "API server auth key", - "url": None, - "password": True, - "category": "messaging", - "advanced": True, - }, - "API_SERVER_PORT": { - "description": "Port for the API server (default: 8642).", - "prompt": "API server port", - "url": None, - "password": False, - "category": "messaging", - "advanced": True, - }, - "API_SERVER_HOST": { - "description": "Host/bind address for the API server (default: 127.0.0.1). API_SERVER_KEY is still required even on loopback binds.", - "prompt": "API server host", - "url": None, - "password": False, - "category": "messaging", - "advanced": True, - }, - "API_SERVER_MODEL_NAME": { - "description": "Model name advertised on /v1/models. Defaults to the profile name (or 'hermes-agent' for the default profile). Useful for multi-user setups with OpenWebUI.", - "prompt": "API server model name", - "url": None, - "password": False, - "category": "messaging", - "advanced": True, - }, - "GATEWAY_PROXY_URL": { - "description": "URL of a remote Hermes API server to forward messages to (proxy mode). When set, the gateway handles platform I/O only — all agent work is delegated to the remote server. Use for Docker E2EE containers that relay to a host agent. Also configurable via gateway.proxy_url in config.yaml.", - "prompt": "Remote Hermes API server URL (e.g. http://192.168.1.100:8642)", - "url": None, - "password": False, - "category": "messaging", - "advanced": True, - }, - "GATEWAY_PROXY_KEY": { - "description": "Bearer token for authenticating with the remote Hermes API server (proxy mode). Must match the API_SERVER_KEY on the remote host.", - "prompt": "Remote API server auth key", - "url": None, - "password": True, - "category": "messaging", - "advanced": True, - }, - "WEBHOOK_ENABLED": { - "description": "Enable the webhook platform adapter for receiving events from GitHub, GitLab, etc.", - "prompt": "Enable webhooks (true/false)", - "url": None, - "password": False, - "category": "messaging", - }, - "WEBHOOK_PORT": { - "description": "Port for the webhook HTTP server (default: 8644).", - "prompt": "Webhook port", - "url": None, - "password": False, - "category": "messaging", - }, - "WEBHOOK_SECRET": { - "description": "Global HMAC secret for webhook signature validation (overridable per route in config.yaml).", - "prompt": "Webhook secret", - "url": None, - "password": True, - "category": "messaging", - }, + "TELEGRAM_BOT_TOKEN": _env( + "Complete Telegram bot token created by @BotFather (numeric bot ID followed by a colon and secret)", + "Telegram bot token", url="https://t.me/BotFather", password=True, category="messaging"), + "TELEGRAM_ALLOWED_USERS": _env( + "Optional comma-separated numeric Telegram user IDs allowed immediately; leave blank to approve " + "new users through DM pairing", "Allowed Telegram user IDs (comma-separated)", + url="https://t.me/userinfobot", password=False, category="messaging"), + "TELEGRAM_PROXY": _env( + "Proxy URL for Telegram connections (overrides HTTPS_PROXY). Supports http://, https://, socks5://", + "Telegram proxy URL (optional)", password=False, category="messaging"), + "DISCORD_BOT_TOKEN": _env("Discord bot token from Developer Portal", "Discord bot token", + url="https://discord.com/developers/applications", password=True, category="messaging"), + "DISCORD_ALLOWED_USERS": _env("Comma-separated Discord user IDs allowed to use the bot", + "Allowed Discord user IDs (comma-separated)", url=None, password=False, category="messaging"), + "DISCORD_REPLY_TO_MODE": _env( + "Discord reply threading mode: 'off' (no reply references), 'first' (reply on first message only, " + "default), 'all' (reply on every chunk)", "Discord reply mode (off/first/all)", url=None, + password=False, category="messaging"), + "SLACK_BOT_TOKEN": _env( + "Slack bot token (xoxb-). Get from OAuth & Permissions after installing your app. Required scopes: " + "chat:write, app_mentions:read, channels:history, groups:history, im:history, im:read, im:write, " + "mpim:history, mpim:read, users:read, files:read, files:write", "Slack Bot Token (xoxb-...)", + help="In your Slack app, add the required bot scopes, install the app to the workspace, then copy OAuth & Permissions > Bot User OAuth Token.", + url="https://api.slack.com/apps", password=True, category="messaging"), + "SLACK_APP_TOKEN": _env( + "Slack app-level token (xapp-) for Socket Mode. Get from Basic Information → App-Level Tokens. " + "Also ensure Event Subscriptions include: message.im, message.channels, message.groups, " + "message.mpim, app_mention", "Slack App Token (xapp-...)", + help="In your Slack app, enable Socket Mode, then create Basic Information > App-Level Tokens with the connections:write scope.", + url="https://api.slack.com/apps", password=True, category="messaging"), + "SLACK_ALLOWED_USERS": _env( + "Comma-separated Slack member IDs allowed to use Hermes, e.g. U01ABC2DEF3. Without this, Slack may " + "connect but deny messages by default.", "Allowed Slack member IDs", + help="In Slack, open your profile, choose More or the three-dot menu, then Copy member ID. Add multiple IDs comma-separated.", + url="https://api.slack.com/apps", password=False, category="messaging"), + "MATTERMOST_URL": _env("Mattermost server URL (e.g. https://mm.example.com)", "Mattermost server URL", + url="https://mattermost.com/deploy/", password=False, category="messaging"), + "MATTERMOST_TOKEN": _env("Mattermost bot token or personal access token", "Mattermost bot token", + url=None, password=True, category="messaging"), + "MATTERMOST_ALLOWED_USERS": _env("Comma-separated Mattermost user IDs allowed to use the bot", + "Allowed Mattermost user IDs (comma-separated)", url=None, password=False, category="messaging"), + "MATTERMOST_REQUIRE_MENTION": _env( + "Require @mention in Mattermost channels (default: true). Set to false to respond to all messages.", + "Require @mention in channels", url=None, password=False, category="messaging"), + "MATTERMOST_FREE_RESPONSE_CHANNELS": _env( + "Comma-separated Mattermost channel IDs where bot responds without @mention", + "Free-response channel IDs (comma-separated)", url=None, password=False, category="messaging"), + "MATRIX_HOMESERVER": _env("Matrix homeserver URL (e.g. https://matrix.example.org)", + "Matrix homeserver URL", url="https://matrix.org/ecosystem/servers/", password=False, + category="messaging"), + "MATRIX_ACCESS_TOKEN": _env("Matrix access token (preferred over password login)", "Matrix access token", + url=None, password=True, category="messaging"), + "MATRIX_USER_ID": _env("Matrix user ID (e.g. @hermes:example.org)", "Matrix user ID (@user:server)", + url=None, password=False, category="messaging"), + "MATRIX_ALLOWED_USERS": _env( + "Comma-separated Matrix user IDs allowed to use the bot (@user:server format)", + "Allowed Matrix user IDs (comma-separated)", url=None, password=False, category="messaging"), + "MATRIX_REQUIRE_MENTION": _env( + "Require @mention in Matrix rooms (default: true). Set to false to respond to all messages.", + "Require @mention in rooms (true/false)", url=None, password=False, category="messaging", + advanced=True), + "MATRIX_FREE_RESPONSE_ROOMS": _env("Comma-separated Matrix room IDs where bot responds without @mention", + "Free-response room IDs (comma-separated)", url=None, password=False, category="messaging", + advanced=True), + "MATRIX_AUTO_THREAD": _env("Auto-create threads for messages in Matrix rooms (default: true)", + "Auto-create threads in rooms (true/false)", url=None, password=False, category="messaging", + advanced=True), + "MATRIX_DM_AUTO_THREAD": _env("Auto-create threads for DM messages in Matrix (default: false)", + "Auto-create threads in DMs (true/false)", url=None, password=False, category="messaging", + advanced=True), + "MATRIX_DEVICE_ID": _env("Stable Matrix device ID for E2EE persistence across restarts (e.g. HERMES_BOT)", + "Matrix device ID (stable across restarts)", url=None, password=False, category="messaging", + advanced=True), + "MATRIX_RECOVERY_KEY": _env( + "Matrix recovery key for cross-signing verification after device key rotation (from Element: " + "Settings → Security → Recovery Key)", "Matrix recovery key", url=None, password=True, + category="messaging", advanced=True), + "BLUEBUBBLES_SERVER_URL": _env( + "BlueBubbles server URL for iMessage integration (e.g. http://192.168.1.10:1234)", + "BlueBubbles server URL", url="https://bluebubbles.app/", password=False, category="messaging"), + "BLUEBUBBLES_PASSWORD": _env("BlueBubbles server password (from BlueBubbles Server → Settings → API)", + "BlueBubbles server password", url=None, password=True, category="messaging"), + "BLUEBUBBLES_ALLOWED_USERS": _env( + "Comma-separated iMessage addresses (email or phone) allowed to use the bot", + "Allowed iMessage addresses (comma-separated)", url=None, password=False, category="messaging"), + "BLUEBUBBLES_ALLOW_ALL_USERS": _env("Allow all BlueBubbles users without allowlist", + "Allow All BlueBubbles Users", category="messaging"), + "QQ_APP_ID": _env("QQ Bot App ID from QQ Open Platform (q.qq.com)", "QQ App ID", url="https://q.qq.com", + category="messaging"), + "QQ_CLIENT_SECRET": _env("QQ Bot Client Secret from QQ Open Platform", "QQ Client Secret", password=True, + category="messaging"), + "QQ_ALLOWED_USERS": _env("Comma-separated QQ user IDs allowed to use the bot", "QQ Allowed Users", + category="messaging"), + "QQ_GROUP_ALLOWED_USERS": _env("Comma-separated QQ group IDs allowed to interact with the bot", + "QQ Group Allowed Users", category="messaging"), + "QQ_ALLOW_ALL_USERS": _env("Allow all QQ users without an allowlist (true/false)", "Allow All QQ Users", + category="messaging"), + "QQBOT_HOME_CHANNEL": _env("Default QQ channel/group for cron delivery and notifications", + "QQ Home Channel", category="messaging"), + "QQBOT_HOME_CHANNEL_NAME": _env("Display name for the QQ home channel", "QQ Home Channel Name", + category="messaging"), + "QQ_SANDBOX": _env("Enable QQ sandbox mode for development testing (true/false)", "QQ Sandbox Mode", + category="messaging"), + "IRC_SERVER": _env("IRC server hostname (e.g. irc.libera.chat)", "IRC server", url=None, password=False, + category="messaging"), + "IRC_CHANNEL": _env("IRC channel to join (e.g. #hermes)", "IRC channel", url=None, password=False, + category="messaging"), + "IRC_NICKNAME": _env("Bot nickname on IRC (default: hermes-bot)", "IRC nickname", url=None, + password=False, category="messaging"), + "IRC_SERVER_PASSWORD": _env("IRC server password (if required)", "IRC server password", url=None, + password=True, category="messaging", advanced=True), + "IRC_NICKSERV_PASSWORD": _env("NickServ password for nick identification", "NickServ password", url=None, + password=True, category="messaging", advanced=True), + "GATEWAY_ALLOW_ALL_USERS": _env( + "Allow all users to interact with messaging bots (true/false). Default: false.", + "Allow all users (true/false)", url=None, password=False, category="messaging", advanced=True), + "API_SERVER_ENABLED": _env( + "Enable the OpenAI-compatible API server (true/false). Allows frontends like Open WebUI, LobeChat, " + "etc. to connect.", "Enable API server (true/false)", url=None, password=False, category="messaging", + advanced=True), + "API_SERVER_KEY": _env( + "Bearer token for API server authentication. Required whenever the API server is enabled; server " + "refuses to start without it.", "API server auth key", url=None, password=True, category="messaging", + advanced=True), + "API_SERVER_PORT": _env("Port for the API server (default: 8642).", "API server port", url=None, + password=False, category="messaging", advanced=True), + "API_SERVER_HOST": _env( + "Host/bind address for the API server (default: 127.0.0.1). API_SERVER_KEY is still required even " + "on loopback binds.", "API server host", url=None, password=False, category="messaging", + advanced=True), + "API_SERVER_MODEL_NAME": _env( + "Model name advertised on /v1/models. Defaults to the profile name (or 'hermes-agent' for the " + "default profile). Useful for multi-user setups with OpenWebUI.", "API server model name", url=None, + password=False, category="messaging", advanced=True), + "GATEWAY_PROXY_URL": _env( + "URL of a remote Hermes API server to forward messages to (proxy mode). When set, the gateway " + "handles platform I/O only — all agent work is delegated to the remote server. Use for Docker E2EE " + "containers that relay to a host agent. Also configurable via gateway.proxy_url in config.yaml.", + "Remote Hermes API server URL (e.g. http://192.168.1.100:8642)", url=None, password=False, + category="messaging", advanced=True), + "GATEWAY_PROXY_KEY": _env( + "Bearer token for authenticating with the remote Hermes API server (proxy mode). Must match the " + "API_SERVER_KEY on the remote host.", "Remote API server auth key", url=None, password=True, + category="messaging", advanced=True), + "WEBHOOK_ENABLED": _env( + "Enable the webhook platform adapter for receiving events from GitHub, GitLab, etc.", + "Enable webhooks (true/false)", url=None, password=False, category="messaging"), + "WEBHOOK_PORT": _env("Port for the webhook HTTP server (default: 8644).", "Webhook port", url=None, + password=False, category="messaging"), + "WEBHOOK_SECRET": _env( + "Global HMAC secret for webhook signature validation (overridable per route in config.yaml).", + "Webhook secret", url=None, password=True, category="messaging"), # ── Agent settings ── # NOTE: MESSAGING_CWD was removed here — use terminal.cwd in config.yaml # instead. The gateway reads TERMINAL_CWD (bridged from terminal.cwd). - "SUDO_PASSWORD": { - "description": "Sudo password for terminal commands requiring root access; set to an explicit empty string to try empty without prompting", - "prompt": "Sudo password", - "url": None, - "password": True, - "category": "setting", - }, + "SUDO_PASSWORD": _env( + "Sudo password for terminal commands requiring root access; set to an explicit empty string to try " + "empty without prompting", "Sudo password", url=None, password=True, category="setting"), # HERMES_TOOL_PROGRESS_MODE is deprecated — tool progress is configured via # display.tool_progress in config.yaml (off|new|all|verbose|log). The # gateway still falls back to HERMES_TOOL_PROGRESS_MODE for backward @@ -5294,18 +4519,10 @@ OPTIONAL_ENV_VARS = { # checklists) and deprecated knobs shouldn't be offered there. The boolean # HERMES_TOOL_PROGRESS is fully unsupported since the v12 config support # floor retired its only consumer (the v3→4 migration). - "HERMES_PREFILL_MESSAGES_FILE": { - "description": "Path to JSON file with ephemeral prefill messages for few-shot priming", - "prompt": "Prefill messages file path", - "url": None, - "password": False, - "category": "setting", - }, - "HERMES_EPHEMERAL_SYSTEM_PROMPT": { - "description": "Ephemeral system prompt injected at API-call time (never persisted to sessions)", - "prompt": "Ephemeral system prompt", - "url": None, - "password": False, - "category": "setting", - }, + "HERMES_PREFILL_MESSAGES_FILE": _env( + "Path to JSON file with ephemeral prefill messages for few-shot priming", + "Prefill messages file path", url=None, password=False, category="setting"), + "HERMES_EPHEMERAL_SYSTEM_PROMPT": _env( + "Ephemeral system prompt injected at API-call time (never persisted to sessions)", + "Ephemeral system prompt", url=None, password=False, category="setting"), } From 2d0d797ceba3d42820d73ff8266806cb307c1c26 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Wed, 2 Sep 2026 15:23:53 -0700 Subject: [PATCH 2/3] refactor(config_defaults): build auxiliary task blocks via _aux() --- hermes_cli/config_defaults.py | 312 +++++++++------------------------- 1 file changed, 84 insertions(+), 228 deletions(-) diff --git a/hermes_cli/config_defaults.py b/hermes_cli/config_defaults.py index 2bd2a35158..ab5fa2d545 100644 --- a/hermes_cli/config_defaults.py +++ b/hermes_cli/config_defaults.py @@ -4,6 +4,20 @@ Pure-data leaf module: DEFAULT_CONFIG and OPTIONAL_ENV_VARS, extracted verbatim hermes_cli/config.py. Must not import from hermes_cli.config. """ + +def _aux(timeout, *, reasoning_effort=True, **extra): + """Standard auxiliary-task model block (see DEFAULT_CONFIG["auxiliary"]). + + reasoning_effort=False omits that key (MoA blocks configure depth per slot); + ``extra`` keys are appended after the standard ones. + """ + d = {"provider": "auto", "model": "", "base_url": "", "api_key": "", "timeout": timeout, "extra_body": {}} + if reasoning_effort: + d["reasoning_effort"] = "" + d.update(extra) + return d + + DEFAULT_CONFIG = { "model": "", "providers": {}, @@ -1167,244 +1181,86 @@ DEFAULT_CONFIG = { # case-insensitive substrings matched against the endpoint URL; # copilot.tencent.com is always treated as stream-only. "stream_only_base_urls": [], - "vision": { - "provider": "auto", # auto | openrouter | nous | codex | custom - "model": "", # e.g. "google/gemini-2.5-flash", "gpt-4o" - "base_url": "", # direct OpenAI-compatible endpoint (takes precedence over provider) - "api_key": "", # API key for base_url (falls back to OPENAI_API_KEY) - "timeout": 120, # seconds — LLM API call timeout; vision payloads need generous timeout - "extra_body": {}, # OpenAI-compatible provider-specific request fields - "reasoning_effort": "", # per-task thinking level: none|minimal|low|medium|high|xhigh|max|ultra (empty = provider default) - "download_timeout": 30, # seconds — image HTTP download timeout; increase for slow connections - }, - # Note: web_extract no longer uses an auxiliary LLM — pages are - # truncate-and-stored with a read_file pointer (no summarization), - # and browser snapshots follow the same pattern. The old - # ``auxiliary.web_extract.*`` block was removed here. Existing - # values in user config.yaml files are harmless leftovers and ignored. - "compression": { - "provider": "auto", - "model": "", - "base_url": "", - "api_key": "", - "timeout": 120, # seconds — compression summarises large contexts; increase for local models - "extra_body": {}, - "reasoning_effort": "", # per-task thinking level: none|minimal|low|medium|high|xhigh|max|ultra (empty = provider default) - # Guarded fast lane: only honored with a concrete provider/model - # and an explicit ``reasoning_effort: none`` certification. - # Zero preserves the historic uncapped compression request. - "max_output_tokens": 0, - }, - # Note: session_search no longer uses an auxiliary LLM (PR #27590 — - # single-shape tool returns DB content directly). The old - # ``auxiliary.session_search.*`` block was removed here. Existing - # values in user config.yaml files are harmless leftovers and ignored. - "skills_hub": { - "provider": "auto", - "model": "", - "base_url": "", - "api_key": "", - "timeout": 30, - "extra_body": {}, - "reasoning_effort": "", # per-task thinking level: none|minimal|low|medium|high|xhigh|max|ultra (empty = provider default) - }, - "approval": { - "provider": "auto", - "model": "", # fast/cheap model recommended (e.g. gemini-flash, haiku) - "base_url": "", - "api_key": "", - "timeout": 30, - "extra_body": {}, - "reasoning_effort": "", # per-task thinking level: none|minimal|low|medium|high|xhigh|max|ultra (empty = provider default) - }, - # /review — the independent reviewer subagent's model. Unlike other - # aux tasks this is not a single LLM call: the reviewer is a full - # subagent (all normal subagent tools) spawned on the async - # delegation rail. provider/model/base_url/api_key/api_mode are - # resolved through the same credential system as delegation.provider - # pins. Leave provider "auto" + model empty to run the reviewer on - # the main agent's model. - "review": { - "provider": "auto", # auto (= inherit main model) | openrouter | nous | anthropic | ... - "model": "", # e.g. "anthropic/claude-opus-4.6" — a strong reviewer model - "base_url": "", # direct OpenAI-compatible endpoint (takes precedence over provider) - "api_key": "", # API key for base_url / provider override - "api_mode": "", # force transport: chat_completions | anthropic_messages | codex_responses - }, - "mcp": { - "provider": "auto", - "model": "", - "base_url": "", - "api_key": "", - "timeout": 30, - "extra_body": {}, - "reasoning_effort": "", # per-task thinking level: none|minimal|low|medium|high|xhigh|max|ultra (empty = provider default) - }, + # Per-task blocks share one shape (see _aux): provider "auto" = inherit the main + # model; base_url takes precedence over provider; api_key falls back to + # OPENAI_API_KEY; reasoning_effort: none|minimal|low|medium|high|xhigh|max|ultra + # (empty = provider default); extra_body = OpenAI-compatible request fields. + # Vision payloads need a generous timeout; download_timeout is the image HTTP + # download (seconds) — raise on slow connections. + "vision": _aux(120, download_timeout=30), + # web_extract and session_search no longer use an auxiliary LLM (pages are + # truncate-and-stored with a read_file pointer; session_search returns DB + # content directly). Leftover auxiliary.web_extract/session_search blocks in + # user config.yaml are ignored. + # Compression summarises large contexts — raise timeout for local models. + # max_output_tokens is a guarded fast lane: only honored with a concrete + # provider/model AND an explicit ``reasoning_effort: none``; 0 = uncapped. + "compression": _aux(120, max_output_tokens=0), + "skills_hub": _aux(30), + # Approval classifier: a fast/cheap model is recommended (gemini-flash, haiku). + "approval": _aux(30), + # /review — the independent reviewer subagent's model. Not a single LLM call: + # a full subagent (all normal subagent tools) spawned on the async delegation + # rail; provider/model/base_url/api_key/api_mode resolve through the same + # credential system as delegation.provider pins. provider "auto" + empty + # model = run on the main agent's model. api_mode forces the transport: + # chat_completions | anthropic_messages | codex_responses. + "review": {"provider": "auto", "model": "", "base_url": "", "api_key": "", "api_mode": ""}, + "mcp": _aux(30), + # prefer_fast_model opts in to the provider fast tier; auto otherwise uses the main model. "title_generation": { "enabled": True, "provider": "auto", "model": "", - "prefer_fast_model": False, # opt in to provider fast tier; auto otherwise uses the main model + "prefer_fast_model": False, "base_url": "", "api_key": "", "timeout": 30, "extra_body": {}, - "reasoning_effort": "", # per-task thinking level: none|minimal|low|medium|high|xhigh|max|ultra (empty = provider default) + "reasoning_effort": "", "language": "", }, - "memory_query_rewrite": { - "provider": "auto", - "model": "", - "base_url": "", - "api_key": "", - "timeout": 8, - "extra_body": {}, - }, - "tts_audio_tags": { - "provider": "auto", - "model": "", - "base_url": "", - "api_key": "", - "timeout": 30, - "extra_body": {}, - "reasoning_effort": "", # per-task thinking level: none|minimal|low|medium|high|xhigh|max|ultra (empty = provider default) - }, - # Triage specifier — flesh out a rough one-liner in the Kanban - # Triage column into a concrete spec, then promote it to ``todo``. - # Invoked by ``hermes kanban specify`` (single id or --all). Set a - # cheap, capable model here (gemini-flash works well); the main - # model is overkill for short spec expansion. - "triage_specifier": { - "provider": "auto", - "model": "", - "base_url": "", - "api_key": "", - "timeout": 120, - "extra_body": {}, - "reasoning_effort": "", # per-task thinking level: none|minimal|low|medium|high|xhigh|max|ultra (empty = provider default) - }, - # Kanban decomposer — decomposes a triage task into a graph of - # child tasks routed to specialist profiles by description. - # Invoked by ``hermes kanban decompose`` and the kanban - # auto-decompose dispatcher tick. Returns a JSON task graph; - # uses more tokens than the specifier so allow more headroom. - "kanban_decomposer": { - "provider": "auto", - "model": "", - "base_url": "", - "api_key": "", - "timeout": 180, - "extra_body": {}, - "reasoning_effort": "", # per-task thinking level: none|minimal|low|medium|high|xhigh|max|ultra (empty = provider default) - }, - # Profile describer — auto-generates a 1-2 sentence description - # of what a profile is good at. Invoked by - # ``hermes profile describe --auto`` and the dashboard's - # auto-generate button. Short, cheap call. - "profile_describer": { - "provider": "auto", - "model": "", - "base_url": "", - "api_key": "", - "timeout": 60, - "extra_body": {}, - "reasoning_effort": "", # per-task thinking level: none|minimal|low|medium|high|xhigh|max|ultra (empty = provider default) - }, - # Goal judge — evaluates whether a /goal run's latest response - # satisfies the goal/contract, and drafts goal contracts. Short - # structured-JSON calls; a fast cheap model is fine. - "goal_judge": { - "provider": "auto", - "model": "", - "base_url": "", - "api_key": "", - "timeout": 60, - "extra_body": {}, - "reasoning_effort": "", # per-task thinking level: none|minimal|low|medium|high|xhigh|max|ultra (empty = provider default) - }, - # Curator — skill-usage review fork. Timeout is generous because the - # review pass can take several minutes on reasoning models (umbrella - # building over hundreds of candidate skills). "auto" = use main chat - # model; override via `hermes model` → auxiliary → Curator to route - # to a cheaper aux model (e.g. openrouter google/gemini-3-flash-preview). - "curator": { - "provider": "auto", - "model": "", - "base_url": "", - "api_key": "", - "timeout": 600, - "extra_body": {}, - "reasoning_effort": "", # per-task thinking level: none|minimal|low|medium|high|xhigh|max|ultra (empty = provider default) - }, - # Monitor — urgency/importance classifier used by the important-mail - # monitor catalog automation (cron/scripts/classify_items.py). Scores - # candidate items 0-10 against the user's criteria so only above- - # threshold items get delivered. "auto" = main chat model; override to - # a cheap fast model (e.g. openrouter google/gemini-3-flash-preview, - # haiku) since per-item scoring is high-volume and a small model is fine. - "monitor": { - "provider": "auto", - "model": "", - "base_url": "", - "api_key": "", - "timeout": 60, - "extra_body": {}, - "reasoning_effort": "", # per-task thinking level: none|minimal|low|medium|high|xhigh|max|ultra (empty = provider default) - }, - # Background review — the post-turn self-improvement fork that decides - # whether to save a memory / patch a skill. "auto" (default) = run on - # the main chat model, replaying the full conversation, which is already - # warm in the prompt cache (cheap cache reads) — unchanged, optimal. - # Set provider/model to a cheaper model (e.g. openrouter - # google/gemini-3-flash-preview) to run the review there for ~3-5x lower - # cost. A different model can't reuse the main prompt cache anyway, so - # the fork automatically replays a compact digest instead of the full - # transcript when routed (minimises the cold-write). Same model = full - # replay; different model = digest. Quality holds (memory capture - # identical, skill near-identical in benchmarks). - "background_review": { - # Master switch for automatic post-turn memory/skill review forks. - # false = skip automatic spawns (manual /refine still works). - "enabled": True, - "provider": "auto", - "model": "", - "base_url": "", - "api_key": "", - "timeout": 120, - "extra_body": {}, - "reasoning_effort": "", # per-task thinking level: none|minimal|low|medium|high|xhigh|max|ultra (empty = provider default) - # Aggregate INPUT-token budget for one review fork (issue #93057). - # The fork's FIRST request replays the full snapshot as a warm - # prompt-cache read (compaction is deferred until the first - # provider response arrives); after that it compacts an oversized - # snapshot in memory before further provider calls. This caps the - # SUM of input tokens replayed across the whole review tool loop - # (iterations are separately capped at 16). The loop stops before - # the provider call that would cross the budget. 0 or a negative - # value = unlimited. - "max_input_tokens": 600000, - }, - "moa_reference": { - "provider": "auto", - "model": "", - "base_url": "", - "api_key": "", - "timeout": 900, - "extra_body": {}, - # NOTE: no reasoning_effort here by design — MoA reasoning depth is - # configured PER SLOT in the MoA preset (moa.presets.. - # reference_models[].reasoning_effort / aggregator.reasoning_effort), - # not at the auxiliary-task level. - }, - "moa_aggregator": { - "provider": "auto", - "model": "", - "base_url": "", - "api_key": "", - "timeout": 900, - "extra_body": {}, - # NOTE: no reasoning_effort here by design — see moa_reference above. - }, + "memory_query_rewrite": _aux(8, reasoning_effort=False), + "tts_audio_tags": _aux(30), + # Triage specifier — expands a rough Kanban Triage one-liner into a concrete + # spec, then promotes it to ``todo`` (``hermes kanban specify``). A cheap, + # capable model suffices. + "triage_specifier": _aux(120), + # Kanban decomposer — turns a triage task into a JSON graph of child tasks + # routed to specialist profiles (``hermes kanban decompose`` and the + # auto-decompose dispatcher tick). Uses more tokens than the specifier. + "kanban_decomposer": _aux(180), + # Profile describer — 1-2 sentence "what this profile is good at" + # (``hermes profile describe --auto``, dashboard button). Short, cheap. + "profile_describer": _aux(60), + # Goal judge — decides whether a /goal run's latest response satisfies the + # goal/contract and drafts contracts. Short structured-JSON calls. + "goal_judge": _aux(60), + # Curator — skill-usage review fork. Generous timeout: the review pass can + # take minutes on reasoning models (umbrella building over hundreds of + # skills). Route to a cheaper aux model via `hermes model` → auxiliary → Curator. + "curator": _aux(600), + # Monitor — urgency/importance classifier for the important-mail monitor + # (cron/scripts/classify_items.py); scores items 0-10 against the user's + # criteria. High-volume per-item scoring — a small fast model is fine. + "monitor": _aux(60), + # Background review — post-turn self-improvement fork deciding whether to + # save a memory / patch a skill. "auto" = main chat model replaying the full + # conversation (warm prompt cache, cheap). A different model cannot reuse + # that cache, so when routed elsewhere the fork replays a compact digest + # instead (~3-5x cheaper; memory capture identical, skill near-identical). + # enabled=false skips automatic spawns (manual /refine still works). + # max_input_tokens caps the SUM of input tokens replayed across the whole + # review tool loop (iterations separately capped at 16): the FIRST request + # replays the full snapshot as a warm cache read, then an oversized snapshot + # is compacted in memory before further calls; the loop stops before the + # provider call that would cross the budget. <= 0 = unlimited. + "background_review": {"enabled": True, **_aux(120), "max_input_tokens": 600000}, + # No reasoning_effort on the MoA blocks by design — MoA reasoning depth is + # configured PER SLOT in the preset (moa.presets..reference_models[] + # .reasoning_effort / aggregator.reasoning_effort), not per aux task. + "moa_reference": _aux(900, reasoning_effort=False), + "moa_aggregator": _aux(900, reasoning_effort=False), }, "display": { From 61c83bafef0bdf1ad4d81b428bd52c07e7060c25 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Wed, 2 Sep 2026 15:56:54 -0700 Subject: [PATCH 3/3] refactor(config_defaults): compact comments by hand (keep rules/invariants, drop history) --- hermes_cli/config_defaults.py | 4443 +++++++++++---------------------- 1 file changed, 1445 insertions(+), 2998 deletions(-) diff --git a/hermes_cli/config_defaults.py b/hermes_cli/config_defaults.py index ab5fa2d545..d3dbe0d585 100644 --- a/hermes_cli/config_defaults.py +++ b/hermes_cli/config_defaults.py @@ -24,540 +24,305 @@ DEFAULT_CONFIG = { "fallback_providers": [], "credential_pool_strategies": {}, "toolsets": ["hermes-cli"], - # SQLite journal mode used by every Hermes database opener. WAL is the - # normal default; set DELETE for weak-fsync/shared filesystems where WAL is - # not crash-safe (for example macOS virtiofs, NFS, or SMB). + # journal_mode: SQLite journal mode for every Hermes DB. "wal" default; use "delete" on + # weak-fsync/shared filesystems where WAL is not crash-safe (macOS virtiofs, NFS, SMB). "database": { "journal_mode": "wal", - # Optional WAL sizing pragmas, applied when set to integers. - # None = SQLite defaults (autocheckpoint 1000 pages, no size limit). + # WAL sizing pragmas (ints). None = SQLite defaults (autocheckpoint 1000 pages, no limit). "wal_autocheckpoint": None, "journal_size_limit": None, }, - # Soft file-descriptor limit for long-running Hermes server processes. - # Clamped to the OS hard limit; 0/false/null disables the adjustment. + # Soft fd limit for long-running server processes; clamped to OS hard limit. 0/false/null = off. "runtime": {"nofile_soft_limit": 4096}, - # Global active chat session cap across CLI, TUI/dashboard, and messaging. - # None/0 = unbounded. + # Global active chat session cap across CLI, TUI/dashboard, and messaging. None/0 = unbounded. "max_concurrent_sessions": None, - # Soft LRU cap on in-memory TUI/desktop/dashboard sessions. When more than - # this many are live, the gateway evicts the least-recently-active DETACHED - # sessions (no live client) so accumulated agents don't pile up under memory - # pressure. Reopening one re-resumes it from disk. 0/null disables. + # Soft LRU cap on in-memory TUI/desktop/dashboard sessions. Above it the gateway evicts the + # least-recently-active DETACHED sessions (no live client); reopening re-resumes from disk. + # 0/null disables. "max_live_sessions": 16, "session": { - # Per-terminal `hermes -c`: each CLI session drops a breadcrumb file - # under $HERMES_HOME/terminal-sessions/, and a bare - # -c/--continue resumes THIS terminal's session (tmux pane, kitty - # window, wezterm pane, plain tty, ...) instead of the globally - # most-recent one. Set false to restore the old latest-session - # behavior everywhere. + # Per-terminal `hermes -c`: each CLI session writes a breadcrumb under + # $HERMES_HOME/terminal-sessions/, so bare -c/--continue resumes THIS + # terminal's session (tmux/kitty/wezterm pane, tty). false = resume globally most-recent. "terminal_continue": True, }, "agent": { - # Unlimited by default. The agent turn cap caused more problems than - # it solved (silent mid-task truncation). null = unlimited; set a - # positive integer to cap, or use "none"/"unlimited"/"inf"/0/-1 — - # all normalized by hermes_cli.config.resolve_turn_limit. + # Turn cap. null = unlimited (default; caps caused silent mid-task truncation). Positive + # int caps; "none"/"unlimited"/"inf"/0/-1 also mean unlimited (resolve_turn_limit). "max_turns": None, - # Optional wall-clock budget in seconds per conversation run. - # null/absent = feature fully off (zero behavior change). When set, - # the agent gets a one-time wrap-up notice at 80% elapsed and - # implicit provider stale timeouts are capped to the remaining - # budget. CLI one-shot equivalent: `hermes chat --run-budget N`. + # Wall-clock budget (seconds) per run. null = off. When set: one-time wrap-up notice at + # 80% elapsed; implicit provider stale timeouts capped to remaining budget. + # CLI equivalent: `hermes chat --run-budget N`. "run_budget_seconds": None, - # Inactivity timeout for gateway agent execution (seconds). - # The agent can run indefinitely as long as it's actively calling - # tools or receiving API responses. Only fires when the agent has - # been completely idle for this duration. 0 = unlimited. + # Gateway inactivity timeout (seconds). Only fires when the agent is completely idle — + # not while calling tools or receiving API responses. 0 = unlimited. "gateway_timeout": 1800, - # Maximum time an alias routing key waits for the active turn holding - # the same resolved session lease. On expiry the inbound message is - # rejected with a resend notice rather than run without serialization. - # Keep this short: Telegram dispatches updates sequentially, so an - # inline lease waiter also delays unrelated topics. Non-positive values - # fall back to the five-second safety default. + # Max seconds an alias routing key waits for the active turn holding the same session + # lease; on expiry the message is rejected with a resend notice. Keep short: Telegram + # dispatches sequentially, so a waiter delays unrelated topics. Non-positive -> 5s. "gateway_turn_lease_timeout": 5, - # Per-session AIAgent cache in the gateway. Each cached agent keeps a - # warm prompt prefix AND the session's full transcript, so the cache - # trades memory for cost: too small and every turn re-pays an uncached - # prompt, too large and tool-heavy transcripts fill the heap. + # Per-session AIAgent cache in the gateway. Each entry keeps a warm prompt prefix AND the + # full transcript: too small re-pays uncached prompts, too large fills the heap. "agent_cache": { - # LRU entry cap. - "max_size": 128, - # Evict an agent that has been idle this long (seconds). - "idle_ttl_secs": 3600, - # Anonymous-RSS budget (MB) above which the gateway starts shedding - # least-recently-used transcripts, which reload from the persisted - # session on the next turn. "auto" derives the budget from the - # cgroup memory limit the gateway runs under (or total RAM when - # uncapped); a number sets it explicitly; 0/off disables the pass - # and lets memory grow to whatever the two bounds above allow. + "max_size": 128, # LRU entry cap + "idle_ttl_secs": 3600, # evict agents idle this long + # Anonymous-RSS budget (MB) above which LRU transcripts are shed (reloaded from + # disk next turn). "auto" = derive from cgroup memory limit (or total RAM); + # number = explicit; 0/off = disable the pass. "memory_high_mb": "auto", - # Upper bound on how many sessions one pressure pass sheds, so a - # burst of teardowns cannot stall the gateway. + # Max sessions shed per pass (teardown bursts can't stall the gateway) and the number + # of most-recently-used sessions the pass never touches. "max_evictions_per_pass": 16, - # Most-recently-used sessions the pressure pass never touches — - # they are the ones actively paying for a warm prompt cache. "protect_recent": 8, }, - # Force-interrupt budget once gateway stop()/drain has begun - # (seconds). Applies to SIGTERM/external stop and to the final - # phase of in-band restart after any after-turn wait. 0 = interrupt - # immediately (the default). - # - # Keep this short and under systemd TimeoutStopSec — a long value - # here invites SIGKILL-mid-cleanup. For in-band restart - # (/restart, SIGUSR1), prefer restart_after_turn_timeout below so - # active turns finish *before* stop() begins (#77184). + # Force-interrupt budget (seconds) once gateway stop()/drain has begun (SIGTERM, and the + # final phase of in-band restart). 0 = interrupt immediately. Keep under systemd + # TimeoutStopSec or risk SIGKILL mid-cleanup; for /restart prefer + # restart_after_turn_timeout so turns finish BEFORE stop(). "restart_drain_timeout": 0, - # Cron-only floor under the stop()/drain wait (seconds). A chat turn - # interrupted by a restart is announced to the user and resumed on - # their next message; an interrupted cron run is written to jobs.json - # as a permanent failure that nobody is waiting on, so it must not - # inherit restart_drain_timeout's 0 (#82161). Clamped at runtime to - # the shutdown-watchdog leash minus teardown headroom, so raising it - # past ~50s has no effect unless TimeoutStopSec is raised too. - # 0 = opt out (cron drains on restart_drain_timeout, legacy). + # Cron-only floor under the stop()/drain wait (seconds). Interrupted chat turns resume on + # the next message, but an interrupted cron run is recorded as a permanent failure, so it + # must not inherit restart_drain_timeout's 0. Clamped to the shutdown-watchdog leash + # minus teardown headroom (~50s unless TimeoutStopSec is raised). 0 = opt out. "cron_drain_timeout": 30, - # In-band restart wait for active turns to finish before stop() - # (seconds). /restart and SIGUSR1 refuse new work, then wait up to - # this cap for in-flight agents/cron/api runs to complete naturally - # so the requesting turn is not amputated by restart_drain_timeout. - # 0 = legacy behaviour (enter stop()/drain immediately). Default - # 30 min is a safety valve for wedged agents, not a target latency — - # an interactive `hermes gateway restart` must never block for hours - # on a turn that wedged (#79133). Long unattended turns can raise - # this in config.yaml. + # In-band restart (/restart, SIGUSR1): refuse new work, then wait up to this many seconds + # for in-flight agents/cron/api runs to finish before stop(). 0 = enter stop() at once. + # 30 min is a safety valve for wedged agents, not a target; raise for long unattended turns. "restart_after_turn_timeout": 1800, - # Upper bound (seconds) a submitted prompt waits for the deferred - # agent build (MCP discovery, model metadata, skills scan) before - # failing with a visible error (#63078). The gateway's wait is - # patient — the prompt is delivered the moment the build completes - # and a progress notice is emitted past 30s — so this cap only fires - # on a genuinely hung build. Raise it for deployments with many slow - # or unreachable MCP servers. + # Max seconds a submitted prompt waits for the deferred agent build (MCP discovery, model + # metadata, skills scan) before failing visibly. The prompt is delivered as soon as the + # build completes (progress notice past 30s), so this only fires on a hung build. Raise + # for many slow/unreachable MCP servers. "build_wait_timeout": 600, - # Max app-level retry attempts for API errors (connection drops, - # provider timeouts, 5xx, etc.) before the agent surfaces the - # failure. The OpenAI SDK already does its own low-level retries - # (max_retries=2 default) for transient network errors; this is - # the Hermes-level retry loop that wraps the whole call. Lower - # this to 1 if you use fallback providers and want fast failover - # on flaky primaries; raise it if you prefer to tolerate longer - # provider hiccups on a single provider. + # Hermes-level retry attempts for API errors (connection drops, timeouts, 5xx) wrapping + # the whole call; the OpenAI SDK also retries transient errors (max_retries=2). Set 1 for + # fast failover to fallback providers; raise to tolerate longer provider hiccups. "api_max_retries": 3, - # Empty-response retry guard (NS-503). The empty-retry loop - # re-sends the full conversation input at full price on every - # attempt; these settings stop it from re-billing *deterministic* - # empties (unsignaled provider refusals with zero output tokens) - # while failing open on any ambiguous evidence (missing usage, - # any generated tokens, model/provider change mid-streak). + # Empty-response retry guard. Empty retries re-send the full input at full price; this + # stops re-billing deterministic empties (unsignaled refusals, zero output tokens) while + # failing open on ambiguous evidence (missing usage, any tokens, model/provider change). "empty_response_guard": { - # Master switch for both guards below. False restores the - # legacy fixed 3-retry behaviour unconditionally. - "enabled": True, - # When the estimated input cost of a single empty attempt - # meets or exceeds this many USD, the retry budget for the - # streak drops from 3 to 1. Unknown pricing or missing usage - # leaves the budget untouched. + "enabled": True, # False = legacy fixed 3 retries unconditionally + # When one empty attempt's estimated input cost >= this USD, the streak's retry + # budget drops from 3 to 1. Unknown pricing / missing usage leaves it untouched. "cost_threshold_usd": 0.25, }, - # Fast mode: "" / "normal" (off), "fast" (always), "auto" (first - # fast_auto_seconds of every turn), "cold" (first turn of a session only). + # Fast mode: "" / "normal" (off), "fast" (always), "auto" (first fast_auto_seconds of + # every turn), "cold" (first turn of a session only). "service_tier": "", "fast_auto_seconds": 60, - # Tool-use enforcement: injects system prompt guidance that tells the - # model to actually call tools instead of describing intended actions. - # Values: "auto" (default — applies to gpt/codex models), true/false - # (force on/off for all models), or a list of model-name substrings - # to match (e.g. ["gpt", "codex", "gemini", "qwen"]). + # System-prompt guidance telling the model to call tools instead of describing actions. + # "auto" = gpt/codex models; true/false = force for all models; or a list of model-name + # substrings (e.g. ["gpt", "codex", "gemini", "qwen"]). "tool_use_enforcement": "auto", - # Execution-discipline guidance: injects a system prompt block covering - # tool persistence, mandatory tool use for arithmetic/system facts, - # external-write read-back, count reconciliation, literal preservation - # of identifiers, and verification-gated completion. Chosen once at - # session start keyed on model name (prompt stays byte-stable). - # Values: "auto" (default — applies to gpt/codex/grok/deepseek/kimi/ - # qwen/glm/minimax/mimo/mistral models), true/false (force on/off for - # all models), or a list of model-name substrings to match. + # Execution-discipline prompt block (tool persistence, tools for arithmetic/system facts, + # read-back after external writes, count reconciliation, literal identifiers, + # verification-gated completion). Chosen once per session by model name (byte-stable). + # "auto" = gpt/codex/grok/deepseek/kimi/qwen/glm/minimax/mimo/mistral; true/false = force; + # or a list of model-name substrings. "execution_guidance": "auto", - # Intent-ack continuation: when the model opens a turn by narrating an - # action it will take ("I'll go check the logs...") but emits no tool - # call, intercept the turn-end, inject a "continue now, execute the - # tools" nudge, and loop instead of ending the turn (capped at 2 nudges - # per turn). This is the corrective sibling of tool_use_enforcement (the - # preventive prompt-side guard). Values: "auto" (default — fires only on - # the codex_responses api_mode, the historical behavior), true (all - # api_modes — fixes the Gemini/Claude "stops after stating intent" case), - # false (never), or a list of model-name substrings to match. + # When the model narrates an action ("I'll go check the logs...") but emits no tool call, + # inject a "continue now, execute the tools" nudge and loop (max 2 nudges/turn). Corrective + # sibling of tool_use_enforcement. "auto" = codex_responses api_mode only; true = all + # api_modes (fixes Gemini/Claude "stops after stating intent"); false = never; or a list + # of model-name substrings. "intent_ack_continuation": "auto", - # Runtime anti-stall guards. When True (default), two conservative - # guards run: (1) an identical-call loop breaker that appends a short - # notice to the tool result when the same tool is called 3+ consecutive - # times with identical arguments AND identical results (never blocks; - # pollers like `process` are exempt), and (2) a continue-intent - # extension of the empty-response recovery that re-prompts once when - # the model ends its turn saying it will continue but takes no action. - # Set False to disable both. + # Anti-stall guards: (1) identical-call loop breaker appends a notice when the same tool + # is called 3+ times with identical args AND results (never blocks; pollers like `process` + # exempt); (2) continue-intent extension of empty-response recovery re-prompts once when + # the model says it will continue but takes no action. False disables both. "stall_guards": True, - # Universal "finish the job" guidance — short prompt block applied to - # all models that targets two cross-family failure modes: (1) stopping - # after a stub instead of finishing the artifact, (2) fabricating - # plausible-looking output when a real path is blocked. Costs ~80 - # tokens in the cached system prompt. Set False to disable globally. + # "Finish the job" prompt block for all models: don't stop at a stub, never fabricate output + # when the real path is blocked. ~80 cached tokens. False disables. "task_completion_guidance": True, - # Universal parallel-tool-call guidance — short prompt block applied to - # all models that tells the model to batch independent tool calls - # (reads, searches, web fetches, read-only commands) into one turn - # instead of one call per turn. The runtime already runs independent - # calls concurrently, so this just steers the model to produce the - # batch — cutting round-trips and the resent-context cost that - # compounds over a long conversation. Costs ~70 tokens in the cached - # system prompt. Set False to disable globally. + # Prompt block for all models steering independent tool calls (reads, searches, fetches, + # read-only commands) into one batched turn; the runtime already runs them concurrently. + # ~70 cached tokens. False disables. "parallel_tool_call_guidance": True, - # Local-environment toolchain probe — surfaces Python/pip/uv/PEP-668 - # state in the system prompt when something non-default is detected - # (e.g. python3 has no pip module, pip→python version mismatch, PEP - # 668 enforcement without uv). Costs zero tokens when the env is - # clean (probe emits nothing). Skipped for remote terminal backends - # (docker/modal/ssh — they have their own probe). Set False to - # disable entirely. + # Toolchain probe: surfaces Python/pip/uv/PEP-668 state in the system prompt only when + # something non-default is detected (no pip module, pip/python mismatch, PEP 668 without + # uv); zero tokens when clean. Skipped for docker/modal/ssh backends (own probe). "environment_probe": True, - # Bot Mode teammate-messaging protocol section (silent unless a - # profile is managed by the desktop's Bot Mode). + # Bot Mode teammate-messaging protocol section (silent unless desktop Bot Mode manages it). "bot_mode_protocol": True, - # Embedder-supplied environment description appended to the system - # prompt's environment-hints block. Lets a host that wraps Hermes - # (sandbox runner, managed platform) explain the runtime environment - # — proxy, credential handling, mount layout — without editing the - # identity slot (SOUL.md). Empty by default. The HERMES_ENVIRONMENT_HINT - # env var overrides this (build-time/container mechanism). + # Embedder-supplied text appended to the system prompt's environment-hints block, so a + # host wrapping Hermes (sandbox runner, managed platform) can describe proxy/credential/ + # mount layout without editing SOUL.md. Env HERMES_ENVIRONMENT_HINT overrides it. "environment_hint": "", - # Coding posture — on interactive coding surfaces (CLI, TUI, desktop - # app, ACP) in a code workspace, Hermes adds a coding operating brief - # + a live git/workspace snapshot to the system prompt. See - # agent/coding_context.py. - # "auto" (default) — prompt-only posture when the surface is - # interactive AND cwd is a code workspace. - # Toolsets are never touched; messaging platforms - # unaffected. - # "focus" — auto + collapse the toolset to the lean coding - # set (+ enabled MCP servers) + demote non-coding - # skill categories to names-only in the prompt's - # skill index. Explicit opt-in. - # "on" — force the prompt posture everywhere. - # "off" — disable entirely. + # Coding posture: on interactive coding surfaces (CLI, TUI, desktop, ACP) in a code + # workspace, add a coding brief + live git/workspace snapshot to the system prompt + # (agent/coding_context.py). "auto" = prompt-only when interactive AND cwd is a code + # workspace (toolsets untouched, messaging platforms unaffected); "focus" = auto + collapse + # toolset to the lean coding set (+ enabled MCP servers) + demote non-coding skill + # categories to names-only (explicit opt-in); "on" = force everywhere; "off" = disable. "coding_context": "auto", - # Standing operator instructions for the coding posture. A string (or - # list of strings) appended to the coding brief as an extra stable - # system block — pin project-wide workflow rules here instead of editing - # the shipped brief, e.g. "For UI work, don't run tsc/lint until I - # approve. Clean the diff before you commit and push." Cache-safe: - # takes effect next session. Empty by default. + # Standing operator instructions (string or list) appended to the coding brief as an extra + # stable system block — project-wide workflow rules, e.g. "Don't run tsc/lint until I + # approve." Cache-safe: takes effect next session. "coding_instructions": "", - # When verify-on-stop finds edited code without fresh verification - # evidence, append guidance for creative UI work (avoid broad - # tsc/lint/test before visual approval) and clean-diff expectations. - # Set false to keep the evidence nudge terse. + # When verify-on-stop finds edits without fresh verification evidence, add guidance for + # creative UI work (no broad tsc/lint/test before visual approval) and clean-diff + # expectations. false = keep the evidence nudge terse. "verify_guidance": True, - # Upper bound on consecutive `pre_verify` "continue" nudges in a single - # turn, so a user/plugin hook can never trap the loop. + # Max consecutive `pre_verify` "continue" nudges per turn (hooks can't trap the loop). "max_verify_nudges": 3, - # Verification closure: after the agent edits files in a code workspace, - # do not accept a final answer until fresh verification evidence exists - # or the agent explains why it cannot run checks. The loop is bounded - # and uses the passive verification ledger. Default is False (opt-in): - # the v31/v32 config migrations already switch existing installs off - # because the verification narrative proved more noise than signal, - # and the docs tell users to treat off as the effective default — a - # fresh install must not be the one population that still gets the - # nudges. Set true to force on everywhere, or "auto" for the legacy - # surface-aware behavior (on for interactive coding surfaces — CLI, - # TUI, desktop — and programmatic callers, off for conversational - # messaging surfaces). Doc/markdown/skill-only edits never fire it. + # Verification closure: after code edits in a workspace, refuse a final answer until fresh + # verification evidence exists or the agent explains why it can't check (bounded loop, + # passive ledger). False (default) because the nudges proved more noise than signal; + # true = force on everywhere; "auto" = on for interactive coding surfaces and programmatic + # callers, off for messaging surfaces. Doc/markdown/skill-only edits never fire. "verify_on_stop": False, - # Staged inactivity warning: send a warning to the user at this - # threshold before escalating to a full timeout. The warning fires - # once per run and does not interrupt the agent. 0 = disable warning. + # Inactivity warning (seconds), once per run before gateway_timeout; no interrupt. 0 = off. "gateway_timeout_warning": 900, - # Maximum time (seconds) the gateway will block an agent waiting for - # a clarify-tool response from the user. Hit this and the agent - # unblocks with "[user did not respond within Xm]" so it can adapt - # rather than pinning the running-agent guard forever. CLI clarify - # blocks indefinitely (input() is synchronous) and ignores this. - # Default 3600 (1h): real users step away (meetings, AFK) and the - # old 600s default evicted the entry mid-think, so a later button - # tap landed on a dead entry (#32762). Tradeoff: a higher value - # holds the gateway's running-agent guard longer for a genuinely - # abandoned prompt — lower it if a single session must free up the - # guard sooner. + # Max seconds the gateway blocks an agent awaiting a clarify-tool reply; then it unblocks + # with "[user did not respond within Xm]". CLI clarify blocks indefinitely and ignores + # this. 1h because users step away and a shorter value evicted the entry mid-think so a + # later button tap hit a dead entry. Lower it to free the running-agent guard sooner. "clarify_timeout": 3600, - # Periodic "still working" notification interval (seconds). - # Sends a status message every N seconds so the user knows the - # agent hasn't died during long tasks. 0 = disable notifications. - # Lower values mean faster feedback on slow tasks but more chat - # noise; 180s is a compromise that catches spinning weak-model runs - # (60+ tool iterations with tiny output) before users assume the - # bot is dead and /restart. + # "Still working" status interval (seconds); 0 = off. Lower = faster feedback, more noise; + # 180 catches spinning weak-model runs before users /restart. "gateway_notify_interval": 180, - # Session stall watchdog (seconds). Scope (#76354): this is a - # RECOVERY notifier for an in-process AIAgent that has an - # adapter-queued follow-up (pending inbound / queued event) while its - # activity clock is stale — NOT a general gateway/session stall - # detector. It does not observe startup restoration, build sentinels, - # turn leases, debounce state, or work owned by another process; the - # scan cadence is per AIAgent instance, not globally coordinated per - # durable session. Notify-only: warns the user to try /new. Distinct - # from gateway_timeout (which kills the turn) and - # gateway_notify_interval ("still working" heartbeats). 0 = disable. + # Session stall watchdog (seconds): RECOVERY notifier for an in-process AIAgent with an + # adapter-queued follow-up while its activity clock is stale — NOT a general stall + # detector (ignores startup restore, build sentinels, leases, debounce, other processes; + # scan cadence per AIAgent). Notify-only: tells the user to try /new. Distinct from + # gateway_timeout (kills the turn) and gateway_notify_interval. 0 = disable. "session_stall_timeout": 300, - # Transcript-sanitiser repeated-heal escalation threshold (#96870). - # After this many pre-send heal passes within a 10-minute session - # window, log one ERROR (session id + heal pattern) and queue a - # ONE-TIME out-of-band user notice pointing at /debug share or - # `hermes doctor`. Delivered via the status channel only — - # conversation context / prompt caching untouched. 0 = disable - # escalation (per-window WARNINGs still fire). + # Transcript-sanitiser heal escalation: after this many pre-send heal passes within a + # 10-minute window, log one ERROR and queue a ONE-TIME out-of-band notice pointing at + # /debug share or `hermes doctor` (status channel only; prompt cache untouched). + # 0 = no escalation (per-window WARNINGs still fire). "sanitizer_heal_escalation_threshold": 3, - # Long-lived reconnect-loop escalation (seconds). A platform that has - # been continuously failing/reconnecting for this long gets - # needs_attention flagged in gateway runtime status (visible in - # `hermes status` / fleet monitoring). Retries never stop — this is a - # signal, not a circuit breaker. 0 = disable. + # Seconds of continuous reconnect failure before a platform gets needs_attention flagged + # in gateway status (`hermes status` / fleet monitoring). Retries never stop — a signal, + # not a circuit breaker. 0 = disable. "reconnect_attention_after": 7200, - # Freshness window for the gateway auto-continue note (seconds). - # After a gateway crash/restart/SIGTERM mid-run, the next user - # message gets a "[System note: your previous turn was - # interrupted — process the unfinished tool result(s) first]" - # prepended so the model picks up where it left off. That's the - # right behaviour while the interruption is fresh, but stale - # markers (transcript last touched hours or days ago) can revive - # an unrelated old task when the user's next message starts new - # work. This window is the max age of the last persisted - # transcript row for which we still inject the continue note. - # Default 3600s comfortably covers a long turn (gateway_timeout - # default is 1800s) plus runtime slack. Set to 0 to disable the - # gate and restore pre-fix behaviour (always inject). + # Freshness window (seconds) for the auto-continue note. After a crash/restart mid-run the + # next user message gets "[System note: your previous turn was interrupted...]" prepended; + # only when the last persisted transcript row is younger than this, so stale markers don't + # revive an unrelated old task. Covers gateway_timeout (1800) plus slack. 0 = always inject. "gateway_auto_continue_freshness": 3600, - # Max seconds the gateway waits for boot auto-resume turns to finish - # before it releases the startup-restore inbound gate. While startup - # restore is in progress the gateway QUEUES every inbound message - # instead of replying, so no channel gets an answer until this gate - # opens. Without a bound, one pathologically long resumed turn holds - # the gate shut and every channel's inbound piles up unanswered for as - # long as that turn runs. On timeout the gate releases and the slow - # resume turn keeps running in the background; duplicate-agent - # protection is unaffected because the resume slot is claimed - # synchronously before the gate runs. Set to 0 to disable the bound - # (historical "wait forever" behaviour). + # Max seconds the gateway waits for boot auto-resume turns before releasing the + # startup-restore inbound gate (all inbound is QUEUED while shut, so one long resumed + # turn would leave every channel unanswered). On timeout the gate opens and the resume + # keeps running in the background; duplicate-agent protection is unaffected because the + # resume slot is claimed synchronously first. 0 = wait forever. "gateway_startup_restore_drain_timeout": 30, - # Max seconds the boot turn-machinery warm-up (#99373) may hold the - # gateway's inbound gate shut. On a fresh boot the gateway warms the - # agent-side turn prerequisites (run_agent import graph, tool schemas - # + availability probes, context-file tier) BEFORE accepting inbound - # messages, so a message seconds after boot is no longer served with - # a skeleton system prompt (missing context files / tool schemas). - # On timeout the gate opens anyway and warm-up finishes in the - # background — a wedged init can't make the gateway permanently - # unavailable. Set to 0 to disable the warm-up (historical - # lazy-init behaviour). + # Max seconds the boot turn-machinery warm-up (run_agent import graph, tool schemas + + # availability probes, context-file tier) may hold the inbound gate shut, so an early + # message isn't served with a skeleton system prompt. On timeout the gate opens and + # warm-up finishes in the background. 0 = disable warm-up (lazy init). "gateway_startup_warmup_timeout": 20, - # Stale-stream ceiling for local providers (Ollama, oMLX, llama-cpp) in - # seconds. When the base stale timeout is at its default (180s) and a - # local endpoint is detected, this finite ceiling replaces the former - # infinite disable so a wedged local server eventually trips the - # detector instead of hanging forever. The env var - # ``HERMES_LOCAL_STREAM_STALE_TIMEOUT`` overrides for escape-hatch use. + # Stale-stream ceiling (seconds) for local providers (Ollama, oMLX, llama-cpp). Applied + # when the base stale timeout is at its 180s default and a local endpoint is detected, so + # a wedged local server eventually trips the detector instead of hanging forever. + # Env HERMES_LOCAL_STREAM_STALE_TIMEOUT overrides. "local_stream_stale_timeout": 900, - # How user-attached images are presented to the main model on each turn. - # "auto" — attach natively when the active model reports - # supports_vision=True AND the user hasn't explicitly - # configured auxiliary.vision.provider. Otherwise fall - # back to text (vision_analyze pre-analysis). - # "native" — always attach natively; non-vision models will either - # error at the provider or get a last-chance text fallback - # (see run_agent._prepare_messages_for_api). - # "text" — always pre-analyze with vision_analyze and prepend the - # description as text; the main model never sees pixels. - # Affects gateway platforms, the TUI, and CLI /attach. vision_analyze - # remains available as a tool regardless of this setting — the routing - # only controls how inbound user images are presented. + # How user-attached images reach the main model (gateway, TUI, CLI /attach). "auto" = + # native when the model reports supports_vision=True AND auxiliary.vision.provider is not + # explicitly set, else text; "native" = always attach (non-vision models error at the + # provider or get a last-chance text fallback); "text" = always pre-analyze with + # vision_analyze and prepend the description. vision_analyze stays a tool regardless. "image_input_mode": "auto", "disabled_toolsets": [], - - # Per-model reasoning effort overrides (spelling-tolerant). - # Dict mapping model names (any reasonable spelling) to effort levels. - # Takes precedence over agent.reasoning_effort when the current model - # matches a key in this dict. - # Edit directly in config.yaml (no CLI support due to dots in keys). + # Model name (any reasonable spelling) -> effort level; overrides agent.reasoning_effort + # when the current model matches. Edit in config.yaml (no CLI support: dots in keys). "reasoning_overrides": {}, - - # Per-provider opt-in to preserve assistant ``reasoning_content`` - # when replaying history. The built-in echo families (DeepSeek, - # Kimi/Moonshot, Xiaomi MiMo) are auto-detected by provider name - # and base-URL host. Custom providers and OpenAI-compatible - # gateways that proxy those same models (or other thinking-mode - # backends) are not covered by the host-based rules. - # - # Set ``reasoning_echo: true`` on a ``model:`` entry (primary) or a - # ``fallback_providers:`` entry (per-fallback) to preserve - # ``reasoning_content`` on replay for that provider only. Default - # ``false`` keeps the historical strict-provider behavior (Mistral, - # Groq, Cerebras reject the field with HTTP 400). + # Preserve assistant `reasoning_content` on history replay. Echo families (DeepSeek, + # Kimi/Moonshot, Xiaomi MiMo) are auto-detected by provider name/base-URL host; custom + # providers and OpenAI-compatible gateways proxying them are not. Set + # `reasoning_echo: true` on a `model:` entry or a `fallback_providers:` entry to opt in + # per provider. Default false: strict providers (Mistral, Groq, Cerebras) reject the field. "reasoning_echo": False, - # Turn liveness watchdog (#95548): a turn that shows no observable - # progress (activity-clock idle, never touched by lease renewal) for - # `timeout_s` seconds is logged loudly, force-interrupted so the UI - # can retry it, and its durable turn lease stops renewing so TTL - # expiry lets stale-turn cleanup reclaim the session even when the - # hard interrupt cannot unwind a wedged frame. `timeout_s` <= 0 - # disables the watchdog; `poll_s` is the sampling interval. Invalid - # values (typo, NaN, Inf, non-positive poll) warn and fall back to - # the default instead of crashing startup or silently disabling the - # watchdog. See agent/turn_liveness.py. + # Turn liveness watchdog: a turn with no observable progress for `timeout_s` seconds is + # logged, force-interrupted so the UI can retry, and its lease stops renewing so stale-turn + # cleanup can reclaim the session even if the interrupt can't unwind a wedged frame. + # timeout_s <= 0 disables; poll_s = sampling interval. Invalid values (NaN, Inf, + # non-positive poll) warn and fall back to defaults. See agent/turn_liveness.py. "turn_liveness": {"timeout_s": 600.0, "poll_s": 15.0}, }, "terminal": { "backend": "local", "modal_mode": "auto", - # Remote-backend graceful degradation: when a connection-class - # infrastructure failure occurs (SSH host unreachable, Docker daemon - # down), "warn" (default) returns a structured degraded tool result - # with a reason + retry hint so the model can act on it; "fail" - # preserves the historical error + traceback behavior. + # Remote-backend connection-class failures (SSH host unreachable, Docker daemon down): + # "warn" = structured degraded tool result with reason + retry hint; "fail" = raise + # error + traceback. "degraded_mode": "warn", "cwd": ".", # Use current directory - # Root directory for Hermes' terminal session temp files (background - # logs/pid/exit files, code-execution sandboxes, etc.). When empty, - # Hermes uses TMPDIR/TMP/TEMP if set, otherwise a managed dir on real - # storage at HERMES_HOME/cache/terminal (auto-pruned after 72h) — NOT - # tmpfs /tmp, which is RAM-capped and small on many distros (e.g. - # Arch-based setups) and fills up under load. Set this to redirect - # session temp anywhere else; must be an absolute POSIX path that - # exists. User-set paths are never auto-pruned. + # Root for terminal session temp files (background logs/pid/exit files, code-exec + # sandboxes). Empty = TMPDIR/TMP/TEMP if set, else HERMES_HOME/cache/terminal + # (auto-pruned after 72h) — NOT tmpfs /tmp, which is RAM-capped and fills under load. + # Must be an existing absolute POSIX path; user-set paths are never auto-pruned. "temp_dir": "", - # Terminal font family for the desktop app's embedded xterm.js terminal. - # When set (e.g. "'CaskaydiaCoveNerdFont', 'JetBrains Mono', monospace"), - # the desktop terminal uses this as the CSS font-family value, with the - # built-in default ("'JetBrains Mono', 'Cascadia Code', 'SF Mono', Menlo, - # Consolas, monospace") as fallback when the field is empty or unset. - # This lets users install a Nerd Font (or any custom font) and configure - # it here without patching the built desktop app. + # CSS font-family for the desktop app's xterm.js terminal (e.g. "'CaskaydiaCoveNerdFont', + # monospace"). Empty = built-in default ("'JetBrains Mono', 'Cascadia Code', 'SF Mono', + # Menlo, Consolas, monospace"). Lets users use a Nerd Font without patching the app. "font_family": "", "timeout": 180, - # Bounded grace period (seconds) between SIGTERM and an escalated - # SIGKILL when terminating a host process tree (browser daemons, etc.). - # A daemon that stalls in its SIGTERM handler is force-killed after this - # window so it can't leak indefinitely. 0 disables escalation (SIGTERM - # only — the historical behavior). Floored internally at 0. + # Seconds between SIGTERM and escalated SIGKILL for host process trees (browser daemons). + # 0 = SIGTERM only. "daemon_term_grace_seconds": 2.0, - # Bounded linger (seconds) for one-shot CLI runs (-q/-Q/-z) that exit - # while background processes spawned with notify_on_complete=true are - # still running. The dying parent owns those children's stdout pipes, - # so exiting immediately kills the delivery a few seconds later — - # destroying Bot Mode handoff replies dispatched via message_agent / - # bot_relay from a short-lived `hermes -p chat -Q` recipient - # (#90879). The parent instead waits (up to this bound) for tracked - # notify_on_complete processes to finish before exiting. Plain - # background processes without notify_on_complete (servers, daemons) - # are never waited on. 0 disables the linger. + # Max seconds a one-shot CLI run (-q/-Q/-z) lingers for tracked notify_on_complete + # background processes to finish. The dying parent owns their stdout pipes, so exiting + # immediately kills the delivery (e.g. Bot Mode handoff replies via message_agent / + # bot_relay). Plain background processes without notify_on_complete are never waited on. + # 0 disables. "oneshot_completion_wait_seconds": 600.0, - # Environment variables to pass through to sandboxed execution - # (terminal and execute_code). Skill-declared required_environment_variables - # are passed through automatically; this list is for non-skill use cases. + # Env vars passed into sandboxed terminal/execute_code (skill-declared + # required_environment_variables pass through automatically). "env_passthrough": [], - # HOME handling for host tool subprocesses: - # auto — host keeps the real OS-user HOME; containers use - # HERMES_HOME/home for persistent state (default) - # real — force the real OS-user HOME - # profile — force HERMES_HOME/home when it exists (old strict - # per-profile CLI config isolation) + # HOME for host tool subprocesses: "auto" = host keeps the real OS-user HOME, containers + # use HERMES_HOME/home; "real" = force real HOME; "profile" = force HERMES_HOME/home when + # it exists (strict per-profile isolation). "home_mode": "auto", - # Extra files to source in the login shell when building the - # per-session environment snapshot. Use this when tools like nvm, - # pyenv, asdf, or custom PATH entries are registered by files that - # a bash login shell would skip — most commonly ``~/.bashrc`` - # (bash doesn't source bashrc in non-interactive login mode) or - # zsh-specific files like ``~/.zshrc`` / ``~/.zprofile``. - # Paths support ``~`` / ``${VAR}``. Missing files are silently - # skipped. When empty, Hermes auto-sources ``~/.profile``, - # ``~/.bash_profile``, and ``~/.bashrc`` (in that order) if the - # snapshot shell is bash (this is the ``auto_source_bashrc`` - # behaviour — disable with that key if you want strict login-only - # semantics). + # Extra files sourced in the login shell when building the per-session env snapshot — + # for nvm/pyenv/asdf/PATH entries registered by files a bash login shell skips + # (~/.bashrc, ~/.zshrc, ~/.zprofile). Supports ~ and ${VAR}; missing files skipped. + # When empty and the shell is bash, ~/.profile, ~/.bash_profile, ~/.bashrc are + # auto-sourced in that order (see auto_source_bashrc). "shell_init_files": [], - # When true (default), Hermes sources the user's shell rc files - # (``~/.profile``, ``~/.bash_profile``, ``~/.bashrc``) in the - # login shell used to build the environment snapshot. This - # captures PATH additions, shell functions, and aliases — which a - # plain ``bash -l -c`` would otherwise miss because bash skips - # bashrc in non-interactive login mode, and because a default - # Debian/Ubuntu ``~/.bashrc`` short-circuits on non-interactive - # sources. ``~/.profile`` and ``~/.bash_profile`` are tried first - # because ``n`` / ``nvm`` / ``asdf`` installers typically write - # their PATH exports there without an interactivity guard. Turn - # this off if your rc files misbehave when sourced - # non-interactively (e.g. one that hard-exits on TTY checks). + # Source ~/.profile, ~/.bash_profile, ~/.bashrc in the snapshot login shell to capture + # PATH additions, functions, and aliases that `bash -l -c` misses (bash skips bashrc when + # non-interactive; Debian/Ubuntu ~/.bashrc short-circuits). ~/.profile and ~/.bash_profile + # go first because n/nvm/asdf write PATH exports there without an interactivity guard. + # Turn off if an rc file misbehaves when sourced non-interactively (exits on TTY check). "auto_source_bashrc": True, "docker_image": "nikolaik/python-nodejs:python3.11-nodejs20", "docker_forward_env": [], - # Explicit environment variables to set inside Docker containers. - # Unlike docker_forward_env (which reads values from the host process), - # docker_env lets you specify exact key-value pairs — useful when Hermes - # runs as a systemd service without access to the user's shell environment. + # Exact key-value env pairs set inside Docker containers (unlike docker_forward_env, which + # reads host values) — useful under systemd without the user's shell env. # Example: {"SSH_AUTH_SOCK": "/run/user/1000/ssh-agent.sock"} "docker_env": {}, "singularity_image": "docker://nikolaik/python-nodejs:python3.11-nodejs20", "modal_image": "nikolaik/python-nodejs:python3.11-nodejs20", "daytona_image": "nikolaik/python-nodejs:python3.11-nodejs20", - # Vercel Sandbox runtime (vercel_sandbox backend only). - # Supported: node24, node22, python3.13. - "vercel_runtime": "node24", - # Container resource limits (docker, singularity, modal, daytona, vercel_sandbox — ignored for local/ssh) + "vercel_runtime": "node24", # vercel_sandbox backend only: node24 | node22 | python3.13 + # Container limits (docker, singularity, modal, daytona, vercel_sandbox; not local/ssh). "container_cpu": 1, "container_memory": 5120, # MB (default 5GB) "container_disk": 51200, # MB (default 50GB) "container_persistent": True, # Persist filesystem across sessions - # Docker volume mounts — share host directories with the container. - # Each entry is "host_path:container_path" (standard Docker -v syntax). - # Example: - # ["/home/user/projects:/workspace/projects", - # "/home/user/.hermes/cache/documents:/output"] - # For gateway MEDIA delivery, write inside Docker to /output/... and emit - # the host-visible path in MEDIA:, not the container path. + # Docker volume mounts, "host_path:container_path" (docker -v syntax), e.g. + # ["/home/user/.hermes/cache/documents:/output"]. For gateway MEDIA delivery, write to + # /output/... inside Docker and emit the host-visible path in MEDIA:, not the container one. "docker_volumes": [], - # Explicit opt-in: mount the host cwd into /workspace for Docker sessions. - # Default off because passing host directories into a sandbox weakens isolation. - "docker_mount_cwd_to_workspace": False, - # Opt-in egress lockdown for Docker terminal sessions. When false, - # Docker runs with --network=none so commands cannot reach the network. - "docker_network": True, + "docker_mount_cwd_to_workspace": False, # mount host cwd at /workspace (weakens isolation) + "docker_network": True, # false = --network=none, no network access from commands "docker_extra_args": [], # Extra flags passed verbatim to docker run - # /dev/shm size for the Docker sandbox. Docker's 64 MB default silently - # breaks Chromium/Playwright and PyTorch DataLoader workers; tmpfs is - # lazily allocated so the higher ceiling costs nothing until used. - # Set to "" (or "0") to omit the flag and use Docker's default. + # /dev/shm size for the Docker sandbox. Docker's 64 MB default silently breaks + # Chromium/Playwright and PyTorch DataLoader workers; tmpfs is lazily allocated so the + # higher ceiling is free until used. "" or "0" = omit the flag (Docker default). "docker_shm_size": "1g", - # Explicit opt-in: run the Docker container as the host user's uid:gid - # (via `--user`). When enabled, files written into bind-mounted dirs - # (docker_volumes, the persistent workspace, or the auto-mounted cwd) - # are owned by your host user instead of root, which avoids needing - # `sudo chown` after container runs. Default off to preserve behavior - # for images whose entrypoints expect to start as root (e.g. the - # bundled Hermes image, which drops to the `hermes` user via - # s6-setuidgid inside each supervised service). - # When on, SETUID/SETGID caps are omitted from the container since - # no privilege drop is needed. + # Run the container as the host uid:gid (`--user`) so files written to bind mounts + # (docker_volumes, persistent workspace, mounted cwd) are owned by you, not root. Off by + # default for images whose entrypoints must start as root (e.g. the bundled Hermes image, + # which drops to `hermes` via s6-setuidgid). When on, SETUID/SETGID caps are omitted. "docker_run_as_host_user": False, - # Explicit opt-in for trusted profiles to reuse the same Docker - # container identity. Empty preserves the active-profile boundary. + # Trusted profiles sharing one Docker container identity; empty = per-profile boundary. "docker_shared_container_key": "", - # Persistent shell — keep a long-lived bash shell across execute() calls - # so cwd/env vars/shell variables survive between commands. - # Enabled by default for non-local backends (SSH); local is always opt-in - # via TERMINAL_LOCAL_PERSISTENT env var. + # Keep a long-lived bash shell across execute() calls so cwd/env/shell variables survive. + # Applies to non-local backends (SSH); local is opt-in via TERMINAL_LOCAL_PERSISTENT env. "persistent_shell": True, }, @@ -565,270 +330,170 @@ DEFAULT_CONFIG = { "backend": "", # shared fallback — applies to both search and extract "search_backend": "", # per-capability override for web_search (e.g. "searxng") "extract_backend": "", # per-capability override for web_extract (e.g. "native") - "extract_char_limit": 15000, # per-page char budget for web_extract; larger pages truncate + store full text in cache/web - # Keyless free-tier ring: with NO web backend configured or keyed, - # web_search/web_extract rotate round-robin across four vendors' - # public free tiers (exa, parallel, firecrawl, keenable), - # failing over to the next ring vendor on rate limits. Never - # pre-empts a configured or keyed backend. Set false to disable. + # per-page char budget for web_extract; larger pages truncate, full text kept in cache/web + "extract_char_limit": 15000, + # Keyless free-tier ring: with NO web backend configured or keyed, web_search/web_extract + # rotate round-robin across exa, parallel, firecrawl, keenable public free tiers, failing + # over on rate limits. Never pre-empts a configured/keyed backend. false = disable. "keyless_fallback": True, - # One-shot keyless rescue: when the chosen/keyed backend fails a - # web_search/web_extract call, THAT call retries once on the keyless - # free-tier ring — the next call attempts the chosen backend again - # (no sticky failover). Off when keyless_fallback is false. + # One-shot rescue: when the chosen/keyed backend fails a call, THAT call retries once on + # the keyless ring; the next call tries the chosen backend again (no sticky failover). + # Off when keyless_fallback is false. "keyless_rescue": True, - # Per-provider tier selection for vendors with both a keyless - # free endpoint and a keyed paid path (exa, parallel, - # firecrawl, keenable on the ring; tavily is opt-in keyless via - # `hermes tools`, not a ring member). Set by the `hermes tools` - # picker's "Free (keyless)" / "Paid (API key)" rows. - # free — always use the anonymous free endpoint (even with a key) - # paid — always use the keyed path (missing key = error; vendor - # is also excluded from the keyless ring) - # unset — auto: keyed when the API key is present, else the ring + # Per-vendor tier for vendors with both a keyless free endpoint and a keyed paid path + # (exa, parallel, firecrawl, keenable; tavily is opt-in keyless via `hermes tools`, not a + # ring member). Set by the `hermes tools` picker. "free" = always anonymous endpoint even + # with a key; "paid" = always keyed (missing key = error; vendor excluded from the ring); + # unset = keyed when the key is present, else the ring. "provider_tier": {}, - # TTL result caching for web_search + web_extract. Repeat searches - # (same query, same provider) within the TTL are served from an - # in-process memo; repeat extracts of the same URL are served from - # the cache/web full-text store. Concurrent identical searches - # (parallel subagents) coalesce into one vendor request. Only - # successful responses are cached. + # TTL caching for web_search + web_extract: repeat searches (same query + provider) within + # the TTL come from an in-process memo; repeat extracts from the cache/web store. + # Concurrent identical searches coalesce into one vendor request. Only successes cached. "cache_enabled": True, "cache_ttl_minutes": 20, - # Hosts whose pages must always be fetched live, never from the - # extract cache — sites you're actively developing but testing over - # the public internet (staging deploys, tunnel URLs, preview - # builds). Entries match exactly, as "*.wildcard", or as a domain - # suffix ("mysite.dev" also covers "preview.mysite.dev"). - # localhost/private-IP URLs are always exempt automatically. - # cache_exempt_hosts: ["mysite.vercel.app", "*.ngrok-free.app"] + # Hosts always fetched live, never from the extract cache (staging deploys, tunnel URLs, + # preview builds). Entries match exactly, as "*.wildcard", or as a domain suffix + # ("mysite.dev" also covers "preview.mysite.dev"). localhost/private IPs always exempt. "cache_exempt_hosts": [], }, "browser": { - # Browser tool implementation. - # "" — DEFAULT: Browser Use mode when the browser-use CLI - # (or uvx) is available; otherwise the built-in - # browser tools. Camofox setups always keep the - # built-in tools (no CDP surface). - # "browser-use" — force Browser Use mode: one browser_exec tool - # driving the Browser Use CLI 3.0 over any CDP - # backend (local Chrome, cloud browsers) - # "off" — force the built-in browser tools - # (browser_navigate, browser_click, …) + # "" = Browser Use mode when the browser-use CLI (or uvx) is available, else built-in tools + # (Camofox setups always keep built-in tools: no CDP surface); "browser-use" = force one + # browser_exec tool driving the Browser Use CLI over any CDP backend (local Chrome, cloud); + # "off" = force the built-in browser_navigate/browser_click/... tools. "backend": "", "inactivity_timeout": 120, - "command_timeout": 30, # Timeout for browser commands in seconds (screenshot, navigate, etc.) - "snapshot_threshold": 15000, # Max chars before snapshot truncate-and-store (min 1000) - "record_sessions": False, # Auto-record browser sessions as WebM videos - "headed": False, # Local mode: launch Chromium with a visible window (also skips per-turn cleanup so the window persists between turns; idle reaper still applies) - "allow_private_urls": False, # Allow navigating to private/internal IPs (localhost, 192.168.x.x, etc.) - # Local browser engine, for both drivers: - # Browser Use mode (default) — "lightpanda" makes Hermes spawn - # ``lightpanda serve`` per session and point browser_exec at it. - # Built-in tools (backend: off) — passed as ``--engine `` to - # agent-browser v0.25.3+ (with automatic Chrome fallback). - # "auto" — Chrome (default) - # "lightpanda" — Lightpanda (faster navigation, no screenshots) - # "chrome" — explicitly request Chrome - # Ignored while a cloud provider, Camofox, browser.cdp_url or - # browser.use_real_profile is active — `/browser status` and - # `hermes doctor` say so. Also settable via AGENT_BROWSER_ENGINE. + "command_timeout": 30, # seconds per browser command (screenshot, navigate, etc.) + "snapshot_threshold": 15000, # max chars before snapshot truncate-and-store (min 1000) + "record_sessions": False, # auto-record browser sessions as WebM videos + # headed: visible Chromium window (local); skips per-turn cleanup, idle reaper still applies + "headed": False, + "allow_private_urls": False, # allow private/internal IPs (localhost, 192.168.x.x, ...) + # Local browser engine for both drivers. "auto" = Chrome; "lightpanda" = faster navigation, + # no screenshots (Browser Use mode spawns `lightpanda serve` per session; built-in tools + # pass `--engine ` to agent-browser with Chrome fallback); "chrome" = explicit. + # Ignored while a cloud provider, Camofox, cdp_url or use_real_profile is active. + # Also settable via AGENT_BROWSER_ENGINE. "engine": "auto", - "auto_local_for_private_urls": True, # When a cloud provider is set, auto-spawn local Chromium for LAN/localhost URLs instead of sending them to the cloud - "cdp_url": "", # Optional persistent CDP endpoint for attaching to an existing Chromium/Chrome - # Consent to browse with the user's REAL logins for local browsing. - # When true, local browsing (the Browser Use CLI, or the built-in - # browser tools) runs on a Hermes-managed SNAPSHOT of the user's - # ACTIVE default-Chromium profile (Local State -> profile.last_used) — - # its cookies, logins and preferences copied in and re-synced when a - # fresh session launches — driven by Hermes' packaged Chromium. Only - # the active profile is copied. The snapshot is a non-default dir, so it - # sidesteps Chrome 136+'s block on debugging the default profile and - # never contends with the user's running browser. Turning this back off - # deletes the snapshot store (~/.hermes/browser-profile/) so copied - # credentials don't outlive consent. Only Chromium-family default - # browsers are supported (Chrome, Edge, Brave, Brave Origin, Chromium); a non-Chromium - # default (e.g. Firefox) fails closed with a clear message. Default - # false. Also gates the browser_exec ``local`` argument, which forces a - # real-profile local session even under a cloud browser backend. Toggle - # in the desktop Settings → Browser section. + # With a cloud provider, auto-spawn local Chromium for LAN/localhost URLs instead + "auto_local_for_private_urls": True, + "cdp_url": "", # persistent CDP endpoint for attaching to an existing Chromium/Chrome + # Consent to browse with the user's REAL logins locally: runs on a Hermes-managed SNAPSHOT + # of the ACTIVE default-Chromium profile (Local State -> profile.last_used; cookies, logins, + # prefs copied and re-synced per fresh session) driven by Hermes' packaged Chromium. The + # snapshot dir sidesteps Chrome 136+'s default-profile debugging block and never contends + # with the running browser. Turning off deletes ~/.hermes/browser-profile/ so credentials + # don't outlive consent. Chromium-family only (Chrome, Edge, Brave, Brave Origin, + # Chromium); Firefox etc. fails closed. Also gates the browser_exec `local` argument + # (real-profile local session even under a cloud backend). Desktop Settings -> Browser. "use_real_profile": False, - # When real-profile browsing needs the browser closed (Windows: a - # running Chrome/Edge/Brave locks its cookie DB deny-all, so it must be - # fully quit before its profile can be copied), arm the "offer to close - # it" flow. This does NOT auto-kill: when the profile is locked the - # snapshot always blocks and the agent asks the user first; only on - # approval does it run `hermes browser close-profile` (terminates the - # browser process tree bound to that profile, losing unsaved tabs) and - # retry. Still locked afterward → stays blocked, no loop, no auto-kill. - # OFF by default. No effect on macOS/Linux (copy-while-running works). + # Windows only: a running Chrome/Edge/Brave locks its cookie DB, so the profile can't be + # copied. When on, a locked profile still blocks and the agent ASKS first; on approval it + # runs `hermes browser close-profile` (kills that profile's browser tree, unsaved tabs + # lost) and retries once; still locked -> stays blocked, no auto-kill. No effect on + # macOS/Linux (copy-while-running works). "real_profile_autoclose": False, - # Pin WHICH source browser profile directory gets snapshotted for - # real-profile browsing (e.g. "Profile 2"). Unset/empty: follows the - # browser's last-used profile (Local State → profile.last_used). On a - # machine with several profiles (work + personal), last-used roulette - # can silently hand the agent the wrong identity; a pin locks it. A pin - # naming a directory that doesn't exist FAILS CLOSED with a fixable - # message rather than falling back to last-used. + # Pin WHICH source profile directory is snapshotted for real-profile browsing (e.g. + # "Profile 2"). Empty = browser's last-used profile, which on multi-profile machines can + # hand the agent the wrong identity. A pin naming a missing directory FAILS CLOSED. "real_profile_pin": "", - "allow_unsafe_evaluate": False, # Legacy override: when true, browser_console(expression=...) bypasses the restrict_evaluate denylist entirely - "restrict_evaluate": False, # Opt-in denylist blocking sensitive JS primitives (cookies/storage/clipboard/network/form values) in browser_console(expression=...) - # CDP supervisor — dialog + frame detection via a persistent WebSocket. - # Active only when a CDP-capable backend is attached (Browserbase or - # local Chrome via /browser connect). See - # website/docs/developer-guide/browser-supervisor.md. + # restrict_evaluate: opt-in denylist blocking sensitive JS primitives (cookies/storage/ + # clipboard/network/form values) in browser_console(expression=...); + # allow_unsafe_evaluate is the legacy override that bypasses that denylist entirely. + "allow_unsafe_evaluate": False, + "restrict_evaluate": False, + # CDP supervisor: dialog + frame detection over a persistent WebSocket; active only with a + # CDP-capable backend (Browserbase, or local Chrome via /browser connect). + # See website/docs/developer-guide/browser-supervisor.md. "dialog_policy": "must_respond", # must_respond | auto_dismiss | auto_accept - "dialog_timeout_s": 300, # Safety auto-dismiss after N seconds under must_respond + "dialog_timeout_s": 300, # safety auto-dismiss after N seconds under must_respond "camofox": { - # When true, Hermes sends a stable profile-scoped userId to Camofox - # so the server maps it to a persistent Firefox profile automatically. - # When false (default), each session gets a random userId (ephemeral). + # true = send a stable profile-scoped userId so Camofox maps it to a persistent + # Firefox profile; false = random ephemeral userId per session. "managed_persistence": False, - # Optional externally managed Camofox identity. Useful when another - # app owns the visible browser and Hermes should operate in it. + # Externally managed Camofox identity, for when another app owns the visible browser. "user_id": "", "session_key": "", - # Rehydrate tab_id from Camofox before creating a new tab. - "adopt_existing_tab": False, - # Docker Camofox opens page URLs from inside the container. Enable - # this to rewrite loopback page URLs (localhost/127.0.0.1/::1) to a - # host alias while leaving CAMOFOX_URL itself unchanged. + "adopt_existing_tab": False, # rehydrate tab_id from Camofox before creating a tab + # Docker Camofox opens page URLs from inside the container: rewrite loopback page URLs + # (localhost/127.0.0.1/::1) to the host alias; CAMOFOX_URL itself is unchanged. "rewrite_loopback_urls": False, "loopback_host_alias": "host.docker.internal", }, - # Authenticated browser-extension controller lane. When enabled, an - # extension that registers through the gateway can become the exact - # controller for a session's browser_* tools (fail-closed once bound). - # Local API registration additionally requires the API server bearer - # key. developer_mode gates the privileged capabilities - # (browser_cdp / browser_evaluate) — never negotiable without it. + # Authenticated browser-extension controller lane: a registered extension can become the + # exact controller for a session's browser_* tools (fail-closed once bound). Local API + # registration also requires the API server bearer key. developer_mode gates the + # privileged browser_cdp / browser_evaluate capabilities. "extension_control": {"enabled": False, "developer_mode": False}, }, - # Filesystem checkpoints — automatic snapshots before destructive file ops. - # When enabled, the agent takes a snapshot of the working directory once - # per conversation turn (on first write_file/patch call). Use /rollback - # to restore. - # - # Defaults changed in v2 (single shared shadow store, real pruning): - # - enabled: True -> False (opt-in; most users never use /rollback) - # - max_snapshots: 50 -> 20 (now actually enforced via ref rewrite) - # - auto_prune: False -> True (orphans/stale pruned automatically) - # Opt in via ``hermes chat --checkpoints`` or set enabled=True here. + # Filesystem checkpoints: snapshot the working directory once per turn (on the first + # write_file/patch call); restore with /rollback. Opt-in via `hermes chat --checkpoints` or + # enabled=True (most users never use /rollback). Single shared shadow store with real pruning. "checkpoints": { "enabled": False, - # Max checkpoints to keep per working directory. Pre-v2 this only - # limited the `/rollback` listing; v2 actually rewrites the ref and - # garbage-collects older commits. + # Max checkpoints per working directory; enforced by ref rewrite + GC of older commits. "max_snapshots": 20, - # Hard ceiling on total ``~/.hermes/checkpoints/`` size (MB). When - # exceeded, the oldest checkpoint per project is dropped in a - # round-robin pass until total size falls under the cap. - # 0 disables the size cap. + # Hard ceiling on total ~/.hermes/checkpoints/ size (MB); the oldest checkpoint per project + # is dropped round-robin until under the cap. 0 disables. "max_total_size_mb": 500, - # Skip any single file larger than this when staging a checkpoint. - # Prevents accidental snapshotting of datasets, model weights, and - # other large generated assets. 0 disables the filter. + # Skip files larger than this (MB) when staging (datasets, model weights). 0 = no filter. "max_file_size_mb": 10, - # Auto-maintenance: hermes sweeps the checkpoint base at startup - # (at most once per ``min_interval_hours``) and: - # * deletes project entries whose last_touch is older than - # ``retention_days`` - # * GCs the single shared store to reclaim unreachable objects - # * enforces ``max_total_size_mb`` across remaining projects - # * deletes ``legacy-*`` archives older than ``retention_days`` - # - # NOTE: this automatic sweep never deletes "orphan" entries (workdir - # no longer found on disk). A missing workdir at startup is - # ambiguous — it can mean the project was deleted, or that an - # external volume / network share / VPN is simply not mounted yet — - # and this sweep runs unattended, so it must never guess. Orphan - # cleanup is only available via the explicit - # ``hermes checkpoints prune`` command (add ``--keep-orphans`` to - # skip it), where a human is looking at the output. + # Startup sweep (at most once per min_interval_hours): deletes projects whose last_touch + # is older than retention_days, GCs the shared store, enforces max_total_size_mb, deletes + # legacy-* archives older than retention_days. It NEVER deletes orphans (workdir missing + # on disk) — a missing workdir may just be an unmounted volume/VPN, and an unattended + # sweep must not guess. Orphans: `hermes checkpoints prune` (`--keep-orphans` to skip). "auto_prune": True, "retention_days": 7, "min_interval_hours": 24, }, - # Hard cap (chars) for a single automatic context file such as SOUL.md, - # AGENTS.md, CLAUDE.md, .hermes.md, or .cursorrules before Hermes applies - # head/tail truncation. ``null`` (the default) lets the cap scale with the - # model's context window (floor 20K, ceiling 500K) so large-context models - # rarely truncate a project doc. Set a positive integer to pin a fixed cap - # and override the dynamic behavior. Separate from read_file tool limits. + # Hard cap (chars) for one auto-loaded context file (SOUL.md, AGENTS.md, CLAUDE.md, + # .hermes.md, .cursorrules) before head/tail truncation. null = scale with the model's + # context window (floor 20K, ceiling 500K); a positive int pins a fixed cap. + # Separate from read_file limits. "context_file_max_chars": None, - # Maximum characters returned by a single read_file call. Reads that - # exceed this are rejected with guidance to use offset+limit. - # 100K chars ≈ 25–35K tokens across typical tokenisers. + # Max chars per read_file call; larger reads are rejected with offset+limit guidance. + # 100K chars ≈ 25–35K tokens. "file_read_max_chars": 100_000, - # Seconds to wait at agent-build time for in-flight MCP server discovery - # to finish before the agent snapshots its tool list. MCP discovery runs - # in a background thread so a slow/dead server can't freeze startup; this - # bounds how long the first agent build blocks on it. The wait returns - # the INSTANT discovery completes, so users with no MCP servers (the common - # case) or fast servers pay ~0s regardless of this value — the bound is - # only reached when a server is genuinely still connecting. The old 0.75s - # default was a touch short for HTTP/OAuth servers on a cold connect; a - # modest bump lets more of them land in the FIRST turn's snapshot. This is - # only a turn-1 latency/UX knob: a server that misses this window is still - # picked up automatically on the next turn by the between-turns refresh - # (see agent/turn_context.py), so correctness never depends on it. Keep it - # small so a slow/dead server adds little to first-response latency. + # Seconds the first agent build waits for background MCP discovery before snapshotting + # its tool list. Returns the instant discovery completes (no MCP servers → ~0s); the + # bound only bites when a server is still connecting. Turn-1 latency knob only: a + # server that misses it is picked up by the between-turns refresh (agent/turn_context.py), + # so keep it small — a dead server adds this much to first-response latency. "mcp_discovery_timeout": 1.5, - # Single-query (``hermes -q/-z "..."``) variant of mcp_discovery_timeout. - # In one-shot mode there is only ONE turn, so the between-turns late-binding - # refresh never runs: a server that misses the small interactive bound is - # invisible to the LLM for the whole session. This larger bound gives slow - # cold-start servers (npx, uvx, remote HTTP) a chance to land in the one - # tool snapshot. ``thread.join(timeout)`` returns the instant discovery - # completes, so reachable servers only wait for their real handshake time - # while unavailable servers remain bounded. + # Same bound for single-query mode (``hermes -q/-z``). With only ONE turn there is no + # between-turns refresh, so a server that misses the window is invisible for the whole + # session; the larger bound lets slow cold-start servers (npx, uvx, remote HTTP) land. + # Reachable servers still only wait their real handshake time. "mcp_single_query_discovery_timeout": 15.0, - # MCP runtime behavior (distinct from the per-server definitions in - # mcp_servers: and from the auxiliary.mcp side-LLM task settings). + # MCP runtime behavior (distinct from mcp_servers: definitions and auxiliary.mcp). "mcp": { - # Auto-reload MCP connections when config.yaml's mcp_servers section - # changes at runtime (CLI file watcher, default on). - # Set to false to stop the automatic reload: every automatic reload - # rebuilds the agent tool surface and INVALIDATES the provider - # prompt cache (the next message re-sends the full input prefix), - # which is expensive on long-context / high-reasoning models. - # When disabled, the watcher still detects the change and prints - # guidance to apply it deliberately via /reload-mcp. + # Auto-reload MCP connections when config.yaml's mcp_servers changes (CLI watcher). + # Every reload rebuilds the tool surface and INVALIDATES the provider prompt cache + # (next message re-sends the full prefix) — costly on long-context models. When + # false the watcher still detects the change and prints /reload-mcp guidance. "auto_reload_on_config_change": True, }, - # Tool-output truncation thresholds. When terminal output or a - # single read_file page exceeds these limits, Hermes truncates the - # payload sent to the model (keeping head + tail for terminal, - # enforcing pagination for read_file). Tuning these trades context - # footprint against how much raw output the model can see in one - # shot. Ported from anomalyco/opencode PR #23770. - # - # - max_bytes: terminal_tool output cap, in chars - # (default 50_000 ≈ 12-15K tokens). - # - max_lines: read_file pagination cap — the maximum `limit` - # a single read_file call can request before - # being clamped (default 2000). - # - max_line_length: per-line cap applied when read_file emits a - # line-numbered view (default 2000 chars). + # Tool-output truncation. max_bytes: terminal_tool output cap in chars (head+tail kept; + # 50_000 ≈ 12-15K tokens). max_lines: max `limit` one read_file call may request before + # clamping. max_line_length: per-line cap in read_file's line-numbered view (chars). "tool_output": {"max_bytes": 50000, "max_lines": 2000, "max_line_length": 2000}, - # Tool loop guardrails nudge models when they repeat failed or - # non-progressing tool calls. Soft warnings are always-on by default; - # hard stops are opt-in so interactive CLI/TUI sessions keep flowing. + # Tool loop guardrails nudge models that repeat failed/non-progressing tool calls. + # Soft warnings are always on; hard stops are opt-in so interactive sessions keep flowing. "tool_loop_guardrails": { "warnings_enabled": True, "hard_stop_enabled": False, - # Unattended gateway/cron platforms get hard stops by default (nobody - # is present to /stop a model that ignores loop warnings); interactive - # cli/tui/desktop/acp stay warning-only unless hard_stop_enabled. + # Unattended gateway/cron platforms hard-stop by default (nobody can /stop a model + # that ignores warnings); interactive cli/tui/desktop/acp stay warning-only. "non_interactive_hard_stop_enabled": True, "warn_after": {"exact_failure": 2, "same_tool_failure": 3, "idempotent_no_progress": 2}, "hard_stop_after": { @@ -836,378 +501,210 @@ DEFAULT_CONFIG = { "same_tool_failure": 8, "idempotent_no_progress": 5, }, - # Per-turn runaway-loop caps (inspired by Claude Code v2.1.212, - # Week 29, July 2026). Hard ceilings on how many times a runaway-prone - # tool may be called within a SINGLE agent loop (turn); the counters - # reset at the start of every turn, so a legitimate multi-turn session - # is never starved. They are always-on and fire regardless of the - # warn/hard-stop thresholds above. A single turn issuing dozens of web - # searches or spawning dozens of subagents is already pathological, so - # the defaults are low. Set either to 0 to disable that cap (unlimited). + # Per-turn hard ceilings for runaway-prone tools; counters reset every turn, always + # on regardless of the thresholds above. Dozens of searches/subagents in ONE turn + # is already pathological, hence low defaults. 0 = unlimited. "loop_caps": { - "max_web_searches": 50, # max web_search calls per turn (0 = unlimited) - "max_subagents": 50, # max subagents spawned per turn (0 = unlimited) + "max_web_searches": 50, # web_search calls per turn + "max_subagents": 50, # subagents spawned per turn }, }, "compression": { "enabled": True, - "checkpoint_required": False, # Fail closed before lossy compaction unless an - # active memory provider confirms checkpoint API - # compatibility and completes the checkpoint. - "progress_notices": False, # opt-in (#52995): when True, routine compression - # progress statuses (compacting/preflight/pre-API/ - # idle/retry) are delivered to chat gateway - # platforms instead of being suppressed by the - # gateway noise filter. Default False keeps - # routine compression silent-by-design on chat - # surfaces (server-side logging only). Failure - # notices and manual /compress feedback are - # always visible regardless of this setting. - "threshold": 0.50, # compress when context usage exceeds this ratio. - # Models with context windows below 512K are - # floored at 0.75 (raise-only) so compaction - # doesn't fire with half the window still free; - # set this above 0.75 to override the floor. - "threshold_tokens": None, # absolute token cap — when set, compression - # triggers at the lower of the ratio-based - # threshold and this token count. Clamped to - # the model's context length at apply-time. + # checkpoint_required: fail closed before lossy compaction unless an active memory + # provider confirms checkpoint API compatibility and completes the checkpoint. + "checkpoint_required": False, + # progress_notices: when True, routine compression progress statuses (compacting/ + # preflight/pre-API/idle/retry) reach chat gateways instead of being filtered as + # noise. Failure notices and manual /compress feedback are always visible. + "progress_notices": False, + # threshold: compress when context usage exceeds this ratio. Models with windows + # below 512K are floored at 0.75 (raise-only) so compaction doesn't fire with half + # the window free; set above 0.75 to override the floor. + "threshold": 0.50, + # threshold_tokens: absolute token cap — compression triggers at the lower of the + # ratio threshold and this count. Clamped to the model's context length. + "threshold_tokens": None, "target_ratio": 0.20, # fraction of threshold to preserve as recent tail - "tail_mode": "lean", # tail retention policy (#87326): - # "lean" — clamped 2.5%-of-window tail (default) - # (10K floor / 25K cap) plus chunked - # digests, a mechanical anchor index, - # verbatim user messages, and - # session_search recovery pointers in - # the summary. ~3x fewer retained - # tokens after compaction; costs a few - # extra summarizer calls at the - # compaction boundary. - # "legacy" — pre-#87326 0.20×threshold verbatim - # tail (100-240K tokens on big-window - # or raised-threshold setups). - "protect_last_n": 20, # minimum recent messages to keep uncompressed - "min_tail_user_messages": 1, # REAL (actionable) user messages guaranteed to - # survive in the uncompressed tail. 1 = existing - # single last-user anchor (default, behavior- - # preserving); raise to e.g. 3 to keep the last - # 3 real user turns verbatim when bulky tool - # outputs fill the tail token budget. - "max_attempts": 3, # compression retry rounds before a turn gives up - # with "max compression attempts reached". Raise - # (e.g. 6) for tool-schema-heavy sessions where 3 - # rounds cannot clear the request estimate. - # Validated >= 1, hard-capped at 10. - "proactive_prune_tokens": 0, # opt-in trigger (tokens) for the deterministic, - # no-LLM tool-result prune, run independently of - # `threshold` above. On large-window models - # `threshold` (≈50% of the window) rarely fires, - # so old tool output otherwise rides in history - # and is re-sent every turn; a low value like - # 48000 reclaims it early. 0 = off. Recent tail - # protected by `protect_last_n`. Built-in - # compressor only (other engines inherit a no-op). - # NOTE: each committed prune rewrites already-sent - # history, breaking the provider prompt-cache - # prefix — the min_reclaim gate below keeps those - # breaks episodic rather than per-turn. - "proactive_prune_min_result_chars": 8000, # the prune's summarize pass only - # touches tool results larger than this (chars); - # clamped to >= 200 so a generated summary can't - # itself be re-summarized. - "proactive_prune_min_reclaim_tokens": 4096, # a proactive prune only commits - # when it reclaims at least this many tokens - # (measured on the pruned output), then waits - # for a full trigger-sized token runway to - # regrow before rearming. Keeps prompt-cache - # breaks episodic. 0 = no minimum-savings gate. - "micro_compact": False, # opt-in: after each completed turn, fold the - # oldest un-absorbed exchange into a rolling - # summary, amortizing compression cost instead - # of paying it in one batch stall. Default False - # because a pass rewrites already-sent history - # and so breaks the provider prompt-cache prefix - # EVERY turn — the per-turn cache break that - # `proactive_prune_min_reclaim_tokens` above - # exists to avoid. Enable only when you have - # measured that the amortized stall is worth - # more to you than the cached-prefix discount. - # See docs/micro-compaction.md. - "micro_compact_every_n_turns": 1, # cadence: run a pass every Nth completed - # turn. Since each pass costs one prompt-cache - # break, this is the dial for how often that - # cost is paid — 1 reclaims most aggressively - # at one break per turn, 5 trades reclaim rate - # for a fifth of the breaks. Clamped to >= 1. - # Ignored unless `micro_compact` is true. - "micro_compact_defrag_threshold_tokens": 2000, # once the rolling summary - # exceeds this many tokens, the next pass - # re-summarizes the summary itself instead of - # letting it grow without bound. - "hygiene_hard_message_limit": 5000, # gateway session-hygiene force-compress threshold by message count - "hygiene_timeout_seconds": 30, # max seconds gateway waits for pre-agent hygiene compression - # WITHOUT forward progress. The summary call streams, so - # this is an inactivity budget: a slow model still - # producing tokens keeps extending the wait; only a - # silent/hung call is cut off. - "hygiene_total_ceiling_seconds": 600, # absolute cap on the hygiene compression wait even - # while tokens are still moving — bounds a degenerate - # trickle stream. Clamped to >= hygiene_timeout_seconds. - "hygiene_failure_cooldown_seconds": 300, # skip repeated failed hygiene attempts for this session - "hygiene_max_turn_hold_seconds": 10, # max seconds an ARRIVING user turn is held while a - # still-streaming hygiene summary finishes. Distinct from - # hygiene_timeout_seconds (compressor inactivity budget): - # this bounds user-visible latency once real input is - # waiting. Kept well under chat-transport idle timeouts - # (Telegram ~30s). On expiry the turn proceeds - # uncompressed — an availability boundary, not a failure. - # The detached worker keeps its commit admission when its - # commit is watermark-fenced, so the finished summary is - # adopted at the next safe boundary instead of being - # discarded (#97963 — thinking summary models). - "context_timeout_seconds": 120, # inactivity budget for in-agent compress_context - # (conversation loop, /compress, preflight, etc.). - # Same progress-aware semantics as hygiene_timeout_seconds: - # streamed summary tokens extend the wait; only a silent - # worker is cut off. 0 = disable the owned wrapper - # (callers that already pass commit_fence, e.g. gateway - # hygiene, never use this path). - "context_total_ceiling_seconds": 600, # absolute cap on the *pre-commit* - # in-agent compress_context wait (summary / - # stream phase) even while tokens are still - # moving. Clamped to >= context_timeout_seconds - # when the idle budget is > 0. Guarantee: - # the summary phase is bounded by this - # ceiling; an already-started SessionDB - # commit is never abandoned mid-flight — - # if the commit itself runs past the - # ceiling it is logged (WARNING, then - # ERROR) and surfaced to the user via the - # warning channel while the host keeps - # waiting in bounded increments for the - # commit to finish. - "protect_first_n": 3, # non-system head messages always preserved - # verbatim, in ADDITION to the system prompt - # (which is always implicitly protected). Set to - # 0 for long-running rolling-compaction sessions - # where you want nothing pinned except the - # system prompt + rolling summary + recent tail. - "abort_on_summary_failure": False, # When True, auto-compression that fails - # to generate a summary (aux LLM errored / returned - # non-JSON / timed out) aborts entirely instead of - # dropping the middle window with a static - # "summary unavailable" placeholder. Messages are - # preserved unchanged and the session "freezes" at - # its current size until the user runs /compress - # (which bypasses the failure cooldown) or /new. - # Default False matches historical behavior; set to - # True if you'd rather pause than silently lose - # context turns when your aux model is flaky. - "codex_gpt55_autoraise": True, # Historical key name kept for compatibility. - # When True, gpt-5.4 / gpt-5.5 / gpt-5.6 on the - # ChatGPT Codex OAuth route raise their compaction - # trigger to 85% (vs the global `threshold` above). - # Codex hard-caps these families at a 272K window, so - # the default 50% would compact at ~136K and waste half - # the usable context. Set to False to opt back down to - # the global threshold (e.g. 0.50) for those Codex - # sessions. Only this exact route is affected — - # gpt-5.4 / 5.5 / 5.6 on OpenAI's direct API, - # OpenRouter, and Copilot keep the global threshold - # regardless. - "codex_gpt55_autoraise_notice": True, # Display the one-time Codex gpt-5.4/5.5/5.6 - # autoraise banner. Set False to keep the - # 85% threshold autoraise but suppress the - # user-facing notice in CLI/gateway output. - "codex_app_server_auto": "native", # Codex app-server (codex CLI runtime) thread - # compaction mode. The codex agent owns the real - # thread context, so Hermes' summarizer cannot - # shrink it (#36801). native = codex decides when - # to compact its own thread (default); hermes = - # Hermes' compression threshold triggers - # thread/compact/start; off = never auto-trigger - # (codex may still compact natively). - "codex_responses_native": False, # Opt in to OpenAI's server-side compaction - # on the Responses API. Engages ONLY for - # gpt-5.6-family models on api.openai.com or - # the ChatGPT Codex backend; every other - # route/model is unaffected. Hermes' local - # compression stays armed as the fallback. - "codex_responses_compact_threshold": None, # Optional absolute server compaction - # trigger in input tokens. None follows the - # resolved local compression trigger with a - # safety margin. Explicit values only clamp - # downward so the server compacts first. - "in_place": True, # When True, compaction rewrites the message - # list and rebuilds the system prompt WITHOUT - # rotating the session id — the conversation - # keeps one durable id for its whole life - # (no parent_session_id chain, no `name #N` - # renumbering). Eliminates the session-rotation - # bug cluster (#33618 /goal loss, #14238 lost - # response, #33907 orphans, #45117 search gaps, - # #42228 null cwd) — see #38763. Non-destructive: - # the live context is compacted (lossy for what - # the model reloads), but the pre-compaction - # turns are soft-archived under the same id - # (active=0, compacted=1) — still searchable via - # session_search and recoverable, not deleted. - # Default True since 2107b86024; set False to - # restore the legacy rotating-compaction path. - "model_thresholds": {}, # Per-model threshold overrides. Keys are - # substring-matched against the model name - # (longest match wins); values replace the - # global `threshold` for that model, e.g. - # model_thresholds: - # "glm-5.2": 0.40 - # "claude-sonnet": 0.35 - # The small-context floor (0.75 for <512K - # models) still applies on top of overrides - # (raise-only: an override above the floor - # wins; one below it is raised to the floor). - "idle_compact_after_seconds": 0, # Opt-in idle compaction (0 = disabled). - # When > 0, a session that resumes after at - # least this many seconds of inactivity - # compacts its accumulated history up front, - # before the first reply — so a long-lived - # thread resumed hours later doesn't re-read - # its full stale context on every turn. - # Time-based; complements (does not replace) - # the size-based `threshold` above. Skipped - # when the context is already at/below the - # post-compression target (threshold × - # target_ratio) and it honors the same - # failure-cooldown / anti-thrash / per-session - # lock guards as every automatic compaction. - # Example: 1800 = compact after 30 min idle. + # tail_mode: "lean" = clamped 2.5%-of-window tail (10K floor / 25K cap) plus chunked + # digests, anchor index, verbatim user messages and session_search pointers in the + # summary (~3x fewer retained tokens; a few extra summarizer calls at the boundary). + # "legacy" = 0.20×threshold verbatim tail (100-240K tokens on big windows). + "tail_mode": "lean", + "protect_last_n": 20, # minimum recent messages kept uncompressed + # min_tail_user_messages: REAL (actionable) user messages guaranteed to survive in + # the tail. 1 = single last-user anchor; raise (e.g. 3) when bulky tool outputs + # fill the tail budget. + "min_tail_user_messages": 1, + # max_attempts: retry rounds before a turn gives up with "max compression attempts + # reached". Raise (e.g. 6) for tool-schema-heavy sessions. Validated >= 1, cap 10. + "max_attempts": 3, + # proactive_prune_tokens: opt-in trigger (tokens) for the deterministic no-LLM + # tool-result prune, independent of `threshold` (which rarely fires on large + # windows, so old tool output is re-sent every turn); e.g. 48000 reclaims early. + # 0 = off. Tail protected by `protect_last_n`. Built-in compressor only. Each + # committed prune rewrites sent history and breaks the prompt-cache prefix — the + # min_reclaim gate below keeps those breaks episodic. + "proactive_prune_tokens": 0, + # Prune's summarize pass only touches tool results larger than this (chars); + # clamped >= 200 so a generated summary can't be re-summarized. + "proactive_prune_min_result_chars": 8000, + # A prune only commits when it reclaims at least this many tokens, then waits for a + # trigger-sized runway to regrow before rearming. 0 = no minimum-savings gate. + "proactive_prune_min_reclaim_tokens": 4096, + # micro_compact: opt-in — after each turn fold the oldest un-absorbed exchange into a + # rolling summary, amortizing compression cost. Off by default because every pass + # rewrites sent history and breaks the prompt-cache prefix EVERY turn; enable only + # if the amortized stall beats the cached-prefix discount. See docs/micro-compaction.md. + "micro_compact": False, + # Cadence: run a pass every Nth completed turn (1 = one cache break per turn, 5 = + # a fifth of the breaks). Clamped >= 1; ignored unless micro_compact is true. + "micro_compact_every_n_turns": 1, + # Once the rolling summary exceeds this many tokens, the next pass re-summarizes it. + "micro_compact_defrag_threshold_tokens": 2000, + # Gateway session-hygiene force-compress threshold, by message count. + "hygiene_hard_message_limit": 5000, + # Max seconds the gateway waits for pre-agent hygiene compression WITHOUT forward + # progress. Inactivity budget: a slow model still streaming tokens extends the wait. + "hygiene_timeout_seconds": 30, + # Absolute cap on the hygiene wait even while tokens are moving (bounds a trickle + # stream). Clamped >= hygiene_timeout_seconds. + "hygiene_total_ceiling_seconds": 600, + "hygiene_failure_cooldown_seconds": 300, # skip repeated failed hygiene attempts + # Max seconds an ARRIVING user turn is held while a streaming hygiene summary + # finishes; bounds user-visible latency (keep under chat idle timeouts, Telegram + # ~30s). On expiry the turn proceeds uncompressed; the detached worker keeps its + # watermark-fenced commit, so the summary is adopted at the next safe boundary. + "hygiene_max_turn_hold_seconds": 10, + # Inactivity budget for in-agent compress_context (loop, /compress, preflight); + # same progress-aware semantics as hygiene_timeout_seconds. 0 = disable the owned + # wrapper (callers passing commit_fence, e.g. gateway hygiene, never use it). + "context_timeout_seconds": 120, + # Absolute cap on the *pre-commit* compress_context wait (summary/stream phase) even + # while tokens move. Clamped >= context_timeout_seconds when that is > 0. A started + # SessionDB commit is never abandoned: past the ceiling it is logged (WARNING, then + # ERROR) and surfaced on the warning channel while the host keeps waiting. + "context_total_ceiling_seconds": 600, + # Non-system head messages always kept verbatim, in ADDITION to the (always + # protected) system prompt. 0 = pin nothing but system prompt + summary + tail. + "protect_first_n": 3, + # When True, auto-compression whose summary fails (aux error / non-JSON / timeout) + # aborts instead of dropping the middle with a "summary unavailable" placeholder; + # the session freezes at its size until /compress (bypasses the cooldown) or /new. + "abort_on_summary_failure": False, + # (Historical key name.) When True, gpt-5.4/5.5/5.6 on the ChatGPT Codex OAuth route + # raise their compaction trigger to 85%: Codex hard-caps them at a 272K window, so + # the global 50% would compact at ~136K. False = global `threshold`. Only that route; + # the same models via OpenAI direct, OpenRouter or Copilot keep the global value. + "codex_gpt55_autoraise": True, + # Show the one-time autoraise banner; False keeps the autoraise, hides the notice. + "codex_gpt55_autoraise_notice": True, + # Codex app-server thread compaction mode. The codex agent owns the thread context, + # so Hermes' summarizer cannot shrink it. native = codex decides; hermes = Hermes' + # threshold triggers thread/compact/start; off = never auto-trigger. + "codex_app_server_auto": "native", + # Opt in to OpenAI server-side compaction on the Responses API. Only gpt-5.6-family + # on api.openai.com or the Codex backend; local compression stays as fallback. + "codex_responses_native": False, + # Absolute server compaction trigger (input tokens). None follows the local trigger + # with a safety margin; explicit values only clamp downward so the server goes first. + "codex_responses_compact_threshold": None, + # in_place: compaction rewrites the message list and system prompt WITHOUT rotating + # the session id (no parent_session_id chain, no `name #N` renumbering), avoiding + # the session-rotation bug cluster. Pre-compaction turns are soft-archived under the + # same id (active=0, compacted=1) — still session_search-able. False = legacy + # rotating-compaction path. + "in_place": True, + # Per-model threshold overrides: keys substring-match the model name (longest wins), + # values replace the global `threshold`, e.g. {"glm-5.2": 0.40}. The <512K floor + # (0.75) still applies raise-only on top. + "model_thresholds": {}, + # Opt-in idle compaction (0 = off): a session resuming after this many idle seconds + # compacts up front, before the first reply. Time-based complement to `threshold`; + # skipped when already at/below threshold × target_ratio; honors the same cooldown/ + # anti-thrash/lock guards. Example: 1800 = 30 min. + "idle_compact_after_seconds": 0, }, - # Anthropic prompt caching (Claude via OpenRouter or native Anthropic API). - # cache_ttl: "5m" or "1h" (Anthropic-supported tiers). Other non-falsy - # values are silently ignored. Falsy values (false, null, "off", - # "disabled", "no", "none") disable prompt caching entirely. + # Anthropic prompt caching (Claude via OpenRouter or native API). cache_ttl: "5m" | "1h"; + # other non-falsy values are ignored; falsy (false, null, "off", "disabled", "no", + # "none") disables caching. "prompt_caching": {"cache_ttl": "5m"}, - # OpenRouter-specific settings. - # response_cache: enable OpenRouter response caching (X-OpenRouter-Cache header). - # When enabled, identical requests return cached responses for free (zero billing). - # This is separate from Anthropic prompt caching and works alongside it. - # See: https://openrouter.ai/docs/guides/features/response-caching - # response_cache_ttl: how long cached responses remain valid, in seconds (1-86400). - # Default 300 (5 minutes). Only used when response_cache is enabled. - # min_coding_score: knob for the openrouter/pareto-code router (0.0-1.0). - # Only applied when model.model is "openrouter/pareto-code". Higher - # values route to stronger (more expensive) coders; lower values open - # up cheaper, faster options. Default 0.65 lands on the mid-tier - # coder on the current Pareto frontier. Empty string = let OpenRouter - # pick the strongest available coder (router's documented default - # when the plugins block is omitted). - # See: https://openrouter.ai/docs/guides/routing/routers/pareto-router + # OpenRouter settings. response_cache: X-OpenRouter-Cache header — identical requests + # return cached responses at zero billing; independent of Anthropic prompt caching. + # response_cache_ttl: seconds (1-86400), only used when response_cache is on. + # min_coding_score (0.0-1.0): pareto-code router knob, applied only when model.model is + # "openrouter/pareto-code"; higher = stronger/pricier coders, 0.65 = mid-tier, "" = let + # OpenRouter pick the strongest. Docs: openrouter.ai/docs/guides/routing/routers/pareto-router "openrouter": {"response_cache": True, "response_cache_ttl": 300, "min_coding_score": 0.65}, - # AWS Bedrock provider configuration. - # Only used when model.provider is "bedrock". + # AWS Bedrock; only used when model.provider is "bedrock". "bedrock": { - "region": "", # AWS region for Bedrock API calls (empty = AWS_REGION env var → us-east-1) + "region": "", # empty = AWS_REGION env var → us-east-1 "discovery": { - "enabled": True, # Auto-discover models via ListFoundationModels - "provider_filter": [], # Only show models from these providers (e.g. ["anthropic", "amazon"]) - "refresh_interval": 3600, # Cache discovery results for this many seconds + "enabled": True, # auto-discover models via ListFoundationModels + "provider_filter": [], # restrict to these providers, e.g. ["anthropic", "amazon"] + "refresh_interval": 3600, # cache discovery results (seconds) }, + # Bedrock Guardrails: create one in the console, then set ID and version. + # https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails.html "guardrail": { - # Amazon Bedrock Guardrails — content filtering and safety policies. - # Create a guardrail in the Bedrock console, then set the ID and version here. - # See: https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails.html "guardrail_identifier": "", # e.g. "abc123def456" "guardrail_version": "", # e.g. "1" or "DRAFT" - "stream_processing_mode": "async", # "sync" or "async" - "trace": "disabled", # "enabled", "disabled", or "enabled_full" + "stream_processing_mode": "async", # "sync" | "async" + "trace": "disabled", # "enabled" | "disabled" | "enabled_full" }, }, - # Auxiliary model config — provider:model for each side task. - # Format: provider is the provider name, model is the model slug. - # "auto" for provider = auto-detect best available provider. - # Empty model = use provider's default auxiliary model. - # All tasks fall back to openrouter:google/gemini-3-flash-preview if - # the configured provider is unavailable. - # - # extra_body: forwarded verbatim as request body fields on every aux call - # for that task. Use this to set provider-specific knobs (independent of - # main-agent settings). On OpenRouter you can set provider routing prefs - # and the Pareto Code coding-score floor here. Example: - # + # Auxiliary model config — provider/model per side task. provider "auto" = auto-detect; + # empty model = provider's default aux model; all tasks fall back to + # openrouter:google/gemini-3-flash-preview when the configured provider is unavailable. + # extra_body is forwarded verbatim as request body fields for that task, e.g. OpenRouter + # routing prefs / Pareto Code floor: # auxiliary: # compression: - # provider: openrouter - # model: openrouter/pareto-code # extra_body: - # provider: # OpenRouter provider routing - # order: [anthropic, google] - # sort: throughput # or price | latency - # plugins: # OpenRouter Pareto Code router - # - id: pareto-router - # min_coding_score: 0.5 - # - # Each aux task is independent — main-agent provider_routing and - # openrouter.min_coding_score do NOT propagate to aux calls by design. + # provider: {order: [anthropic, google], sort: throughput} # or price | latency + # plugins: [{id: pareto-router, min_coding_score: 0.5}] + # Each task is independent — main-agent provider_routing and openrouter.min_coding_score + # do NOT propagate to aux calls by design. "auxiliary": { - # Same-provider retries for a transient transport blip (connection - # reset / timeout / 5xx / 408) on ANY auxiliary call before falling - # back. Default 2 (→ 3 total attempts), clamped [0,6]. Matters most for - # pinned calls like MoA reference advisors, where provider fallback is - # not a meaningful recovery, so an unretried blip silently loses the - # call. + # Same-provider retries for a transient blip (reset/timeout/5xx/408) on ANY aux call + # before falling back; clamped [0,6]. Matters for pinned calls (MoA advisors) where + # provider fallback is not meaningful recovery. "transient_retries": 2, - # Restrict the auxiliary auto-chain's OpenRouter fallback to free - # (:free) SKUs. When true, the OpenRouter step is skipped entirely - # unless the resolved fallback model ends in ":free" — a PAID lane - # is never engaged for background auxiliary traffic (compression, - # title generation, session search, vision, web extract) even when - # OPENROUTER_API_KEY is present. Default false keeps the historical - # paid fallback for users who want it. + # When true, the auto-chain's OpenRouter step is skipped unless the fallback model + # ends in ":free" — a PAID lane is never used for background aux traffic even with + # OPENROUTER_API_KEY set. "free_only": False, - # Override the auxiliary auto-chain's OpenRouter fallback model - # (default: google/gemini-3.6-flash, a PAID model). Set e.g. - # "nvidia/nemotron-3-ultra-550b-a55b:free" together with - # free_only: true to keep auxiliary traffic free-only. A one-time - # WARNING is logged whenever a non-":free" model is engaged. + # Override the auto-chain's OpenRouter fallback model (default google/gemini-3.6-flash, + # PAID). Pair e.g. "nvidia/nemotron-3-ultra-550b-a55b:free" with free_only: true. + # A one-time WARNING is logged whenever a non-":free" model is engaged. "openrouter_model": "", - # Endpoints that reject NON-streaming chat requests outright (e.g. - # Tencent Copilot returns HTTP 400 "Non-stream chat request is - # currently not supported"). Auxiliary calls to a matching endpoint - # are sent with stream=True and aggregated client-side. Entries are - # case-insensitive substrings matched against the endpoint URL; - # copilot.tencent.com is always treated as stream-only. + # Endpoints that reject NON-streaming chat (HTTP 400): aux calls are sent with + # stream=True and aggregated. Case-insensitive URL substrings; copilot.tencent.com + # is always stream-only. "stream_only_base_urls": [], - # Per-task blocks share one shape (see _aux): provider "auto" = inherit the main - # model; base_url takes precedence over provider; api_key falls back to - # OPENAI_API_KEY; reasoning_effort: none|minimal|low|medium|high|xhigh|max|ultra - # (empty = provider default); extra_body = OpenAI-compatible request fields. - # Vision payloads need a generous timeout; download_timeout is the image HTTP - # download (seconds) — raise on slow connections. + # Per-task blocks share one shape (_aux): provider "auto" = inherit the main model; + # base_url overrides provider; api_key falls back to OPENAI_API_KEY; reasoning_effort: + # none|minimal|low|medium|high|xhigh|max|ultra ("" = provider default); extra_body = + # OpenAI-compatible request fields. Vision: download_timeout = image HTTP download (s). "vision": _aux(120, download_timeout=30), - # web_extract and session_search no longer use an auxiliary LLM (pages are - # truncate-and-stored with a read_file pointer; session_search returns DB - # content directly). Leftover auxiliary.web_extract/session_search blocks in - # user config.yaml are ignored. - # Compression summarises large contexts — raise timeout for local models. - # max_output_tokens is a guarded fast lane: only honored with a concrete - # provider/model AND an explicit ``reasoning_effort: none``; 0 = uncapped. + # web_extract and session_search no longer use an aux LLM; leftover blocks in user + # config are ignored. Compression: raise timeout for local models. max_output_tokens + # is only honored with a concrete provider/model AND ``reasoning_effort: none``; + # 0 = uncapped. "compression": _aux(120, max_output_tokens=0), "skills_hub": _aux(30), - # Approval classifier: a fast/cheap model is recommended (gemini-flash, haiku). - "approval": _aux(30), - # /review — the independent reviewer subagent's model. Not a single LLM call: - # a full subagent (all normal subagent tools) spawned on the async delegation - # rail; provider/model/base_url/api_key/api_mode resolve through the same - # credential system as delegation.provider pins. provider "auto" + empty - # model = run on the main agent's model. api_mode forces the transport: - # chat_completions | anthropic_messages | codex_responses. + "approval": _aux(30), # classifier — a fast/cheap model is recommended + # /review reviewer: a full subagent on the async delegation rail, credentials + # resolved like delegation.provider pins. "auto" + "" = main agent's model. + # api_mode forces transport: chat_completions | anthropic_messages | codex_responses. "review": {"provider": "auto", "model": "", "base_url": "", "api_key": "", "api_mode": ""}, "mcp": _aux(30), - # prefer_fast_model opts in to the provider fast tier; auto otherwise uses the main model. + # prefer_fast_model opts in to the provider fast tier; auto otherwise = main model. "title_generation": { "enabled": True, "provider": "auto", @@ -1222,491 +719,280 @@ DEFAULT_CONFIG = { }, "memory_query_rewrite": _aux(8, reasoning_effort=False), "tts_audio_tags": _aux(30), - # Triage specifier — expands a rough Kanban Triage one-liner into a concrete - # spec, then promotes it to ``todo`` (``hermes kanban specify``). A cheap, - # capable model suffices. + # Kanban: triage_specifier expands a Triage one-liner into a spec (cheap model OK); + # kanban_decomposer emits a JSON graph of child tasks (more tokens). "triage_specifier": _aux(120), - # Kanban decomposer — turns a triage task into a JSON graph of child tasks - # routed to specialist profiles (``hermes kanban decompose`` and the - # auto-decompose dispatcher tick). Uses more tokens than the specifier. "kanban_decomposer": _aux(180), - # Profile describer — 1-2 sentence "what this profile is good at" - # (``hermes profile describe --auto``, dashboard button). Short, cheap. - "profile_describer": _aux(60), - # Goal judge — decides whether a /goal run's latest response satisfies the - # goal/contract and drafts contracts. Short structured-JSON calls. - "goal_judge": _aux(60), - # Curator — skill-usage review fork. Generous timeout: the review pass can - # take minutes on reasoning models (umbrella building over hundreds of - # skills). Route to a cheaper aux model via `hermes model` → auxiliary → Curator. + "profile_describer": _aux(60), # 1-2 sentence profile blurb; short, cheap + "goal_judge": _aux(60), # /goal satisfaction + contract drafting; JSON calls + # Curator skill-usage review can take minutes on reasoning models (umbrellas over + # hundreds of skills); route cheaper via `hermes model` → auxiliary → Curator. "curator": _aux(600), - # Monitor — urgency/importance classifier for the important-mail monitor - # (cron/scripts/classify_items.py); scores items 0-10 against the user's - # criteria. High-volume per-item scoring — a small fast model is fine. - "monitor": _aux(60), - # Background review — post-turn self-improvement fork deciding whether to - # save a memory / patch a skill. "auto" = main chat model replaying the full - # conversation (warm prompt cache, cheap). A different model cannot reuse - # that cache, so when routed elsewhere the fork replays a compact digest - # instead (~3-5x cheaper; memory capture identical, skill near-identical). - # enabled=false skips automatic spawns (manual /refine still works). - # max_input_tokens caps the SUM of input tokens replayed across the whole - # review tool loop (iterations separately capped at 16): the FIRST request - # replays the full snapshot as a warm cache read, then an oversized snapshot - # is compacted in memory before further calls; the loop stops before the - # provider call that would cross the budget. <= 0 = unlimited. + "monitor": _aux(60), # important-mail 0-10 scorer; high-volume, small model fine + # Post-turn self-improvement fork (save memory / patch skill). "auto" = main model + # replaying the full conversation (warm cache); other models replay a compact digest + # (~3-5x cheaper). enabled=false skips auto spawns (/refine still works). + # max_input_tokens caps the SUM of replayed input tokens over the review loop + # (iterations capped at 16); the loop stops before crossing it. <= 0 = unlimited. "background_review": {"enabled": True, **_aux(120), "max_input_tokens": 600000}, - # No reasoning_effort on the MoA blocks by design — MoA reasoning depth is - # configured PER SLOT in the preset (moa.presets..reference_models[] - # .reasoning_effort / aggregator.reasoning_effort), not per aux task. + # No reasoning_effort on MoA blocks by design — configured PER SLOT in the preset + # (moa.presets..reference_models[].reasoning_effort / aggregator.reasoning_effort). "moa_reference": _aux(900, reasoning_effort=False), "moa_aggregator": _aux(900, reasoning_effort=False), }, - + "display": { "compact": False, "personality": "", "resume_display": "full", - # Recap tuning for /resume and startup resume. The defaults match the - # historical hardcoded values; expose them as config so power users can - # widen or tighten the snapshot to taste. + # Recap tuning for /resume and startup resume. "resume_exchanges": 10, # max user+assistant pairs to show "resume_max_user_chars": 300, # truncate user message text "resume_max_assistant_chars": 200, # truncate non-last assistant text "resume_max_assistant_lines": 3, # truncate non-last assistant lines - # When True (default), assistant entries that are *only* tool calls - # (no visible text) are skipped in the recap. This prevents the recap - # from being dominated by `[2 tool calls: terminal, read_file]` lines - # when an exchange was tool-heavy. Set False to restore the legacy - # behavior of showing tool-call summaries inline. + # Skip tool-call-only assistant entries in the recap so it isn't dominated by + # `[2 tool calls: ...]` lines; False shows them inline. "resume_skip_tool_only": True, "busy_input_mode": "interrupt", # interrupt | queue | steer - # When busy_input_mode="steer", suppress only the visible - # "Steered into current run" confirmation bubble by setting this false. - # The mid-turn steering itself still happens. + # steer mode: false hides only the "Steered into current run" bubble; steering + # itself still happens. "busy_steer_ack_enabled": True, - # Classic CLI multiline fallbacks beyond Alt+Enter. - # Default true matches Claude Code / Codex / OpenCode: Ctrl+J inserts - # a newline, a trailing backslash followed by Enter continues the draft, - # and supported terminals are asked to report Shift+Enter distinctly. - # Set false to restore the legacy c-j submit fallback on unusual POSIX - # PTYs whose plain Enter arrives as LF instead of CR. + # Classic CLI multiline beyond Alt+Enter: Ctrl+J newline, trailing backslash+Enter + # continues, Shift+Enter reported distinctly. False restores the c-j submit fallback + # for POSIX PTYs whose plain Enter arrives as LF. "cli_multiline_shortcuts": True, - # Which interface bare `hermes` (and `hermes chat`) launches by default: - # "cli" — the classic prompt_toolkit REPL (default, preserves prior behavior) - # "tui" — the modern Ink TUI (same as passing `--tui`) - # Explicit flags always win over this setting: `--cli` forces the classic - # REPL and `--tui` (or HERMES_TUI=1) forces the TUI regardless of config. + # Interface bare `hermes`/`hermes chat` launches: "cli" (prompt_toolkit REPL) | "tui" + # (Ink). Flags win: `--cli` forces the REPL, `--tui` / HERMES_TUI=1 forces the TUI. "interface": "cli", - # When true, `hermes --tui` auto-resumes the most recent human- - # facing session on launch instead of forging a fresh one. - # Mirrors `hermes -c` muscle memory. Default off so existing - # users aren't surprised. HERMES_TUI_RESUME= always wins. + # `hermes --tui` auto-resumes the most recent human-facing session (like `hermes -c`). + # HERMES_TUI_RESUME= always wins. "tui_auto_resume_recent": False, - # When true (default), the Desktop app reopens the last chat (or - # last page) on cold start. Set false to always land on a fresh - # new chat. Also a switch in Desktop Settings → Appearance. + # Desktop reopens the last chat/page on cold start (also in Settings → Appearance). "resume_last_session": True, - # When true (default), `hermes --tui` drops a one-time hint - # ("subagents working · /agents to watch live") the first time a turn - # starts delegating, nudging the user toward the live spawn-tree - # dashboard. Set false to suppress the hint. + # One-time TUI hint ("subagents working · /agents to watch live") on first delegation. "tui_agents_nudge": True, "bell_on_complete": False, - # Bell when a blocking prompt opens (clarify/approval/sudo/secret). - "bell_on_prompt": False, - # Stream the model's reasoning/thinking live before the response. - # Default ON: on thinking models the reasoning phase can run tens of - # seconds, and with this off the user stares at a spinner the whole - # time even though tokens are streaming. Set false for quiet output. + "bell_on_prompt": False, # bell when a blocking prompt opens (clarify/approval/sudo) + # Stream reasoning live before the response; otherwise thinking models show only a + # spinner for tens of seconds. "show_reasoning": True, - # When reasoning display is on, the post-response "Reasoning" recap box - # collapses long thinking to the first 10 lines. Set true to print the - # complete thinking text uncollapsed (live streaming is always full). + # Post-response "Reasoning" recap collapses to 10 lines; true prints it all + # (live streaming is always full). "reasoning_full": False, - # Background self-improvement review notifications surfaced in chat. - # "off" — no chat notification (the review still runs and writes) - # "on" — generic "💾 Memory updated" line (default) - # "verbose" — include a compact content preview of what changed - # Per-platform overrides via display.platforms..memory_notifications. + # Background self-improvement notices in chat: "off" (review still runs) | "on" + # (generic "💾 Memory updated") | "verbose" (content preview). Per-platform via + # display.platforms..memory_notifications. "memory_notifications": "on", - # Gateway notifications when a terminal(background=true) process - # finishes: - # "concise" — one-line status message; failures append a short - # output tail (default) - # "all" — running-output updates + final raw-output message - # "result" — final raw-output message only - # "error" — final raw-output message only on non-zero exit - # "off" — no watcher messages at all + # Gateway notices when a terminal(background=true) process finishes: "concise" + # (one line; failures append an output tail) | "all" (running updates + final raw + # output) | "result" (final raw only) | "error" (raw only on non-zero exit) | "off". "background_process_notifications": "concise", "streaming": False, - "timestamps": False, # Show message timestamps (CLI labels, TUI rows, desktop transcript) - "timestamp_format": "%H:%M", # strftime format for timestamps (e.g. "%b-%d %H:%M") + "timestamps": False, # message timestamps (CLI labels, TUI rows, desktop transcript) + "timestamp_format": "%H:%M", # strftime format, e.g. "%b-%d %H:%M" "final_response_markdown": "strip", # render | strip | raw - # Preserve recent classic CLI output across Ctrl+L, /redraw, and - # terminal resize full-screen clears. Disable if a terminal emulator - # behaves badly with replayed scrollback. + # Preserve recent classic-CLI output across Ctrl+L, /redraw and resize clears; + # disable if an emulator misbehaves with replayed scrollback. "persistent_output": True, "persistent_output_max_lines": 200, - # Clear terminal scrollback as well as the visible viewport when the - # classic CLI performs a full redraw/resize recovery. Disabled by - # default because some users prefer preserving terminal history; - # enable when a terminal/tmux stack stamps stale prompt chrome into - # scrollback during fullscreen/restore window transitions. + # Also clear terminal scrollback on classic-CLI full redraw/resize recovery; enable + # when a terminal/tmux stack stamps stale prompt chrome into scrollback. "cli_rebuild_scrollback_on_redraw": False, - # Print a one-line summary of resolved modal prompts (approval / - # clarify) into scrollback so the question and decision survive the - # panel repaint. Set false to keep scrollback untouched. + # Print a one-line summary of resolved modal prompts (approval/clarify) to scrollback. "persist_prompts": True, - "inline_diffs": True, # Show inline diff previews for write actions (write_file, patch, skill_manage) - # File-mutation verifier footer. When true (default), the agent - # appends a one-line advisory to its final response whenever a - # write_file / patch call failed during the turn and was never - # superseded by a successful write to the same path. This catches - # the "batch of parallel patches, half fail, model claims success" - # class of over-claim that otherwise forces users to run - # `git status` to verify edits landed. Set false to suppress. + "inline_diffs": True, # inline diff previews for write_file/patch/skill_manage + # Append a one-line advisory to the final response when a write_file/patch failed + # this turn and was never superseded by a successful write to the same path + # (catches "half the parallel patches failed, model claims success"). "file_mutation_verifier": True, - # Nous credits status-bar notices (usage bands, grant-spent, depleted / - # restored). When false, no credits notices are emitted — balance data - # is still captured and /usage keeps working. Off switch for sub + - # top-up users who find the gauge noisy. + # Nous credits status-bar notices (usage bands, grant-spent, depleted/restored). + # False mutes them; balance data and /usage keep working. "credits_notices": True, - # Turn-completion explainer. When true (default), the agent appends a - # one-line explanation to its final response whenever a turn ends - # abnormally with no usable reply — empty content after retries, a - # partial/truncated stream, a still-pending tool result, or an - # iteration/budget limit. Replaces the bare "(empty)" sentinel so the - # failure isn't silent from the UI's perspective. Set false to suppress. + # Append a one-line explanation when a turn ends with no usable reply (empty after + # retries, truncated stream, pending tool result, iteration/budget limit) instead of + # the bare "(empty)" sentinel. "turn_completion_explainer": True, - "show_cost": False, # Show $ cost in the status bar (off by default) - # Show a color-coded battery read-out as the first status-bar element in - # the CLI/TUI (off by default). No-op on machines without a battery. - "battery": False, - # Focus view (/focus): display-only reduced-output mode. When true the - # CLI/TUI pins tool_progress to "off" (reusing the existing suppression - # path), reports a per-turn hidden-line count with a recovery hint, and - # pins a "focus" segment in the status bar. focus_saved_tool_progress - # holds the mode /focus off restores. Never affects what is sent to the - # model — see hermes_cli/focus_view.py. + "show_cost": False, # $ cost in the status bar + "battery": False, # battery read-out first in status bar; no-op w/o battery + # Focus view (/focus): display-only. Pins tool_progress to "off", reports per-turn + # hidden-line count, pins a "focus" status segment. focus_saved_tool_progress holds + # the mode /focus off restores. Never affects what the model sees (focus_view.py). "focus_view": False, "focus_saved_tool_progress": "all", "skin": "default", - # UI language for static user-facing messages (approval prompts, a - # handful of gateway slash-command replies). Does NOT affect agent - # responses, log lines, tool outputs, or slash-command descriptions. - # Supported: en, zh, ja, de, es, fr, tr, uk. Unknown values fall back to en. + # UI language for static messages (approval prompts, some gateway slash replies); not + # agent responses/logs/tool outputs. en, zh, ja, de, es, fr, tr, uk; unknown → en. "language": "en", - # TUI busy indicator style: kaomoji (default), emoji, unicode (braille - # spinner), or ascii. Live-swappable via `/indicator