feat(telemetry): expose shared metrics setup

Signed-off-by: Alex Fournier <afournier@nvidia.com>
This commit is contained in:
Alex Fournier 2026-07-23 13:36:00 -07:00
parent 591ba267d2
commit 36185bf2e2
4 changed files with 89 additions and 2 deletions

View file

@ -3241,6 +3241,14 @@ DEFAULT_CONFIG = {
"profile_build": "ask",
},
# Privacy-safe aggregate metrics written only to this profile's local
# telemetry directory. Collection is opt-in and no remote sink exists.
"telemetry": {
"shared_metrics": {
"enabled": False,
},
},
# ``hermes update`` behaviour.
"updates": {
# Pre-update safety backup — ONE consolidated mechanism, three modes:

View file

@ -2199,6 +2199,37 @@ def setup_tools(config: dict, first_install: bool = False):
tools_command(first_install=first_install, config=config)
# =============================================================================
# Shared Metrics
# =============================================================================
def setup_telemetry(config: dict):
"""Configure the local, privacy-safe shared-metrics subscriber."""
print_header("Shared Metrics")
print_info("Shared metrics contain only bounded counters and histograms.")
print_info("Packages stay under this Hermes profile and are not uploaded.")
telemetry = config.get("telemetry")
if not isinstance(telemetry, dict):
telemetry = {}
config["telemetry"] = telemetry
shared_metrics = telemetry.get("shared_metrics")
if not isinstance(shared_metrics, dict):
shared_metrics = {}
telemetry["shared_metrics"] = shared_metrics
current = shared_metrics.get("enabled") is True
shared_metrics["enabled"] = prompt_yes_no(
"Enable local shared metrics?",
default=current,
)
if shared_metrics["enabled"]:
print_success("Local shared metrics enabled.")
else:
print_info("Local shared metrics disabled.")
# =============================================================================
# Post-Migration Section Skip Logic
# =============================================================================
@ -2607,6 +2638,7 @@ SETUP_SECTIONS = [
("terminal", "Terminal Backend", setup_terminal_backend),
("gateway", "Messaging Platforms (Gateway)", setup_gateway),
("tools", "Tools", setup_tools),
("telemetry", "Shared Metrics", setup_telemetry),
("agent", "Agent Settings", setup_agent_settings),
]
@ -2705,6 +2737,7 @@ def run_setup_wizard(args):
hermes setup terminal — just terminal backend
hermes setup gateway — just messaging platforms
hermes setup tools — just tool configuration
hermes setup telemetry — just local shared metrics
hermes setup agent — just agent settings
"""
from hermes_cli.config import is_managed, managed_error

View file

@ -18,12 +18,21 @@ def build_setup_parser(subparsers, *, cmd_setup: Callable) -> None:
"setup",
help="Interactive setup wizard",
description="Configure Hermes Agent with an interactive wizard. "
"Run a specific section: hermes setup model|tts|terminal|gateway|tools|agent",
"Run a specific section: "
"hermes setup model|tts|terminal|gateway|tools|telemetry|agent",
)
setup_parser.add_argument(
"section",
nargs="?",
choices=["model", "tts", "terminal", "gateway", "tools", "agent"],
choices=[
"model",
"tts",
"terminal",
"gateway",
"tools",
"telemetry",
"agent",
],
default=None,
help="Run a specific setup section instead of the full wizard",
)

View file

@ -0,0 +1,37 @@
"""Tests for shared-metrics configuration discovery and setup."""
from __future__ import annotations
import argparse
from hermes_cli.config import DEFAULT_CONFIG
from hermes_cli.setup import setup_telemetry
from hermes_cli.subcommands.setup import build_setup_parser
def test_shared_metrics_are_registered_disabled_by_default():
assert DEFAULT_CONFIG["telemetry"]["shared_metrics"]["enabled"] is False
def test_setup_telemetry_enables_shared_metrics(monkeypatch):
config = {}
monkeypatch.setattr(
"hermes_cli.setup.prompt_yes_no",
lambda _question, default: not default,
)
setup_telemetry(config)
assert config["telemetry"]["shared_metrics"]["enabled"] is True
def test_setup_parser_accepts_telemetry_section():
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest="command")
handler = object()
build_setup_parser(subparsers, cmd_setup=handler)
args = parser.parse_args(["setup", "telemetry"])
assert args.section == "telemetry"
assert args.func is handler