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