# Conflicts: # .gitignore # Dockerfile # agent/onboarding.py # apps/desktop/electron/main.ts # apps/desktop/electron/pool-stop.ts # apps/desktop/src/components/model-picker.test.tsx # apps/desktop/src/store/updates.ts # apps/desktop/vite.config.ts # datagen-config-examples/run_browser_tasks.sh # docs/rca-ssl-cacert-post-git-pull.md # gateway/run.py # hermes_cli/backup.py # hermes_cli/credential_lifecycle.py # hermes_cli/dashboard_procs.py # hermes_cli/doctor_state.py # hermes_cli/env_loader.py # hermes_cli/gateway_windows.py # hermes_cli/local_runtime/endpoint.py # hermes_cli/psutil_android.py # hermes_cli/update_cmd.py # hermes_cli/update_cmd_windows.py # hermes_cli/web_routers/local_models.py # hermes_cli/web_server_config.py # hermes_cli/web_server_cron.py # plugins/memory/hindsight/__init__.py # plugins/memory/holographic/__init__.py # plugins/memory/honcho/cli.py # plugins/memory/mem0/__init__.py # plugins/platforms/google_chat/oauth.py # plugins/platforms/photon/adapter.py # scripts/ci/list_os_marked_tests.py # scripts/run_tests.sh # tests/agent/test_compression_stall_fallback.py # tests/agent/test_create_openai_client_ssl_verify.py # tests/gateway/test_google_chat_oauth_dependencies.py # tests/hermes_cli/conftest.py # tests/hermes_cli/test_cli_init.py # tests/hermes_cli/test_gateway_migrate_multiplex.py # tests/hermes_cli/test_psutil_android_extract.py # tests/hermes_cli/test_relaunch.py # tests/hermes_cli/test_update_check.py # tests/hermes_cli/test_update_handoff_desktop_rebuild.py # tests/hermes_cli/test_worktree_gc.py # tests/scripts/desktop_update/test_desktop_update_windows_python_handoff.py # tests/scripts/desktop_update/test_desktop_update_windows_retry_policy.py # tests/scripts/desktop_update/test_desktop_update_windows_timestamp.py # tests/scripts/install/test_install_autostash_conflict_recovery.py # tests/scripts/install/test_install_clone_throttle_fallback.py # tests/scripts/install/test_install_commit_pin_rollback.py # tests/scripts/install/test_install_diverged_update.py # tests/scripts/install/test_install_lockfile_churn.py # tests/scripts/install/test_install_macos_launcher.py # tests/scripts/install/test_install_no_initial_commit.py # tests/scripts/install/test_install_ps1_ascii_only.py # tests/scripts/install/test_install_ps1_browser_install.py # tests/scripts/install/test_install_ps1_managed_node_swap.py # tests/scripts/install/test_install_ps1_native_stderr_eap.py # tests/scripts/install/test_install_ps1_node_path_for_npm.py # tests/scripts/install/test_install_ps1_python_fallback_venv.py # tests/scripts/install/test_install_ps1_resolver_strictmode.py # tests/scripts/install/test_install_ps1_uv_install_fallback.py # tests/scripts/install/test_install_ps1_uv_powershell_host.py # tests/scripts/install/test_install_ps1_venv_process_tree.py # tests/scripts/install/test_install_ps1_venv_recreate_safety.py # tests/scripts/install/test_install_ps1_venv_rename_abort.py # tests/scripts/install/test_install_ps1_venv_transaction_boundary.py # tests/scripts/install/test_install_ps1_web_server_syntax_probe.py # tests/scripts/install/test_install_scripts_computer_use.py # tests/scripts/install/test_install_sh_acp_launcher.py # tests/scripts/install/test_install_sh_bootstrap_marker.py # tests/scripts/install/test_install_sh_browser_install.py # tests/scripts/install/test_install_sh_install_method_stamp.py # tests/scripts/install/test_install_sh_node_deps_failure.py # tests/scripts/install/test_install_sh_node_deps_workspaces.py # tests/scripts/install/test_install_sh_node_global_prefix.py # tests/scripts/install/test_install_sh_node_npm_check.py # tests/scripts/install/test_install_sh_node_prerelease.py # tests/scripts/install/test_install_sh_node_probe.py # tests/scripts/install/test_install_sh_node_tarball_without_xz.py # tests/scripts/install/test_install_sh_pythonpath_sanitization.py # tests/scripts/install/test_install_sh_reuse_supported_python.py # tests/scripts/install/test_install_sh_root_fhs_uv_python_path.py # tests/scripts/install/test_install_sh_setup_wizard_tty_probe.py # tests/scripts/install/test_install_sh_symlink_stomp.py # tests/scripts/install/test_install_sh_termux_network_prereqs.py # tests/scripts/install/test_install_sh_termux_python_bounds.py # tests/scripts/install/test_install_sh_uv_lock_config.py # tests/scripts/install/test_install_unmerged_index.py # tests/scripts/test_run_tests_parallel.py # tests/test_managed_runtime_resolution.py # tests/test_project_metadata.py # tests/tools/test_browser_use_cli.py # tests/tools/test_tts_pythonpath_fallback.py # tests/tui_gateway/test_hosted_room_driver_runtime.py # tests/tui_gateway/test_tui_gateway_server.py # tools/lazy_deps.py # tools/voice_mode.py # uv.lock # website/docs/developer-guide/macos-bundle-updates.md # website/docs/developer-guide/pm-audit-status.md # website/docs/developer-guide/shared-bundle-builds.md # website/docs/developer-guide/source-update-completion.md # website/docs/developer-guide/stable-releases.md
195 lines
5.5 KiB
Python
195 lines
5.5 KiB
Python
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_otlp_attrs_redact_strings_and_never_export_profile():
|
|
from agent.monitoring.otlp_exporter import _span_attrs
|
|
|
|
attrs = _span_attrs({
|
|
"event": "gateway_health",
|
|
"name": "gateway.lifecycle",
|
|
"profile": "user@example.com",
|
|
"exit_reason": "Bearer top-secret-token-0123456789 for user@example.com",
|
|
})
|
|
|
|
assert "hermes.profile" not in attrs
|
|
assert "top-secret-token-0123456789" not in str(attrs)
|
|
assert "user@example.com" not in str(attrs)
|
|
|
|
|
|
def test_resource_attributes_are_allowlisted_and_sanitized():
|
|
from agent.monitoring.otlp_exporter import _safe_resource_attributes
|
|
|
|
attrs = _safe_resource_attributes({
|
|
"service.name": "hermes-gateway",
|
|
"service.instance.id": "install-1",
|
|
"deployment.environment.name": "staging",
|
|
"user.email": "user@example.com",
|
|
"authorization": "Bearer top-secret-token-0123456789",
|
|
"custom.request.id": "unbounded",
|
|
})
|
|
|
|
assert attrs == {
|
|
"service.name": "hermes-gateway",
|
|
"service.instance.id": attrs["service.instance.id"],
|
|
"deployment.environment.name": "staging",
|
|
}
|
|
assert attrs["service.instance.id"].startswith("sha256:")
|
|
assert "install-1" not in attrs["service.instance.id"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_diagnostic_log_attributes_are_allowlisted_redacted_and_profile_free():
|
|
from agent.monitoring.gateway_health_export import _diagnostic_log_attributes
|
|
|
|
attrs = _diagnostic_log_attributes({
|
|
"event": "gateway_diagnostic",
|
|
"name": "platform.fatal",
|
|
"subsystem": "platform.slack",
|
|
"profile": "user@example.com",
|
|
"error_code": "Bearer top-secret-token-0123456789",
|
|
"custom": "must-not-egress",
|
|
})
|
|
|
|
assert "hermes.profile" not in attrs
|
|
assert "hermes.custom" not in attrs
|
|
assert "top-secret-token-0123456789" not in str(attrs)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_gateway_health_metrics_reach_loopback_collector():
|
|
"""Exercise the real SDK loader, metric provider and OTLP transport together."""
|
|
import threading
|
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
|
|
import pytest
|
|
|
|
pytest.importorskip("opentelemetry.sdk.metrics", reason="otlp extra not installed")
|
|
from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ExportMetricsServiceRequest
|
|
from agent.monitoring.gateway_health_export import start_gateway_health_export
|
|
|
|
received = []
|
|
delivered = threading.Event()
|
|
|
|
class Collector(BaseHTTPRequestHandler):
|
|
def do_POST(self):
|
|
payload = self.rfile.read(int(self.headers["Content-Length"]))
|
|
received.append((self.path, ExportMetricsServiceRequest.FromString(payload)))
|
|
self.send_response(200)
|
|
self.send_header("Content-Length", "0")
|
|
self.end_headers()
|
|
delivered.set()
|
|
|
|
def log_message(self, *args):
|
|
pass
|
|
|
|
server = ThreadingHTTPServer(("127.0.0.1", 0), Collector)
|
|
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
thread.start()
|
|
runtime = None
|
|
try:
|
|
runtime = start_gateway_health_export({"monitoring": {
|
|
"install_id": "test-install",
|
|
"export": {"otlp": {"enabled": True, "endpoint": f"http://127.0.0.1:{server.server_port}/v1/traces"}},
|
|
"gateway_health_export": {
|
|
"enabled": True, "metrics_enabled": True, "diagnostic_events_enabled": False,
|
|
},
|
|
}})
|
|
assert runtime.enabled, runtime.reason
|
|
assert runtime.metric_provider.force_flush(timeout_millis=10000)
|
|
assert delivered.wait(timeout=10)
|
|
assert all(path == "/v1/metrics" for path, _ in received)
|
|
names = {metric.name for _, request in received
|
|
for resource in request.resource_metrics
|
|
for scope in resource.scope_metrics for metric in scope.metrics}
|
|
assert "hermes.gateway.up" in names
|
|
finally:
|
|
if runtime is not None:
|
|
runtime.shutdown()
|
|
server.shutdown()
|
|
server.server_close()
|
|
thread.join(timeout=10)
|
|
|
|
|
|
def test_start_gateway_health_export_reports_otlp_unavailable_when_sdk_missing(monkeypatch):
|
|
import agent.monitoring.gateway_health_export as gw
|
|
from agent.monitoring.otlp_exporter import OTLPUnavailable
|
|
|
|
config = {
|
|
"monitoring": {
|
|
"export": {"otlp": {"enabled": True, "endpoint": "http://127.0.0.1:4318"}},
|
|
"gateway_health_export": {"enabled": True, "metrics_enabled": True, "diagnostic_events_enabled": False},
|
|
}
|
|
}
|
|
|
|
def no_sdk(*args, **kwargs):
|
|
raise OTLPUnavailable("missing")
|
|
|
|
monkeypatch.setattr(gw.otlp_exporter, "_require_sdk", no_sdk)
|
|
|
|
runtime = gw.start_gateway_health_export(config)
|
|
|
|
assert runtime.enabled is False
|
|
assert runtime.reason == "otlp_unavailable"
|
|
|
|
|
|
def test_install_id_persists_across_calls(tmp_path, monkeypatch):
|
|
"""A minted install id must survive restarts (service.instance.id continuity)."""
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
|
(tmp_path / "config.yaml").write_text("{}\n")
|
|
|
|
import hermes_cli.config as cfg_mod
|
|
from agent.monitoring.policy import ensure_install_id
|
|
|
|
first = ensure_install_id(cfg_mod.load_config())
|
|
assert first and first != "unknown"
|
|
# Persisted: a fresh load (simulating a new gateway process) returns the same id.
|
|
second = ensure_install_id(cfg_mod.load_config())
|
|
assert second == first
|
|
assert first in (tmp_path / "config.yaml").read_text()
|
|
|
|
|