mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-23 10:42:00 +00:00
feat(computer_use): disable cua-driver telemetry by default, add opt-in (#50842)
* feat(computer_use): disable cua-driver telemetry by default, add opt-in cua-driver ships anonymous PostHog usage telemetry ENABLED by default upstream (fires cua_driver_install / cua_driver_doctor events to eu.i.posthog.com). Hermes now disables it for our users unless they explicitly opt in. - New config key `computer_use.cua_telemetry` (default false) in DEFAULT_CONFIG. - `cua_backend.cua_driver_child_env()` injects `CUA_DRIVER_RS_TELEMETRY_ENABLED=0` into the child env when telemetry is disabled (the default); leaves the var untouched on opt-in so the driver uses its own default. Reads config fail-safe — any error defaults to telemetry off. - Routed every cua-driver spawn site through the policy: MCP backend (StdioServerParameters env), `cua_driver_update_check`, doctor's health_report Popen, the install.sh/install.ps1 runner, and the `--version` / status probes. - Docs: new Telemetry subsection in computer-use.md (EN). - Tests: tests/computer_use/test_cua_telemetry.py — default disables, explicit-false disables, opt-in leaves var untouched, config-failure fails safe, inherited-enabled is overridden off. Verified live on Linux against the real cua-driver-rs 0.6.0 binary: with the var=0 the driver reports "telemetry: disabled via CUA_DRIVER_RS_TELEMETRY_ENABLED" and sends no event; with it unset it logs "sending event: cua_driver_doctor". 213 computer_use + install tests green. * fix(dashboard): fold computer_use config category into agent tab The new computer_use.cua_telemetry key created a single-field dashboard config category, tripping test_no_single_field_categories (web_server's invariant that categories with <2 fields must be merged to avoid tab sprawl). Add computer_use -> agent to _CATEGORY_MERGE, matching the existing onboarding/telegram single-field folds.
This commit is contained in:
parent
ed711e1c2c
commit
f1e6d39a74
8 changed files with 195 additions and 5 deletions
|
|
@ -2794,6 +2794,17 @@ DEFAULT_CONFIG = {
|
|||
"paste_collapse_threshold_fallback": 5,
|
||||
"paste_collapse_char_threshold": 2000,
|
||||
|
||||
# Computer Use (cua-driver) toolset settings.
|
||||
"computer_use": {
|
||||
# cua-driver ships with anonymous usage telemetry (PostHog) ENABLED
|
||||
# by default upstream. Hermes disables it for our users unless they
|
||||
# explicitly opt in here. When false (default), Hermes sets
|
||||
# CUA_DRIVER_RS_TELEMETRY_ENABLED=0 in the cua-driver child env for
|
||||
# every invocation (MCP backend, status, doctor, install). Set true
|
||||
# to let cua-driver use its own default (telemetry on).
|
||||
"cua_telemetry": False,
|
||||
},
|
||||
|
||||
|
||||
# Config schema version - bump this when adding new required fields
|
||||
"_config_version": 30,
|
||||
|
|
|
|||
|
|
@ -12526,9 +12526,11 @@ def main():
|
|||
if path:
|
||||
version = ""
|
||||
try:
|
||||
from hermes_cli.tools_config import _cua_driver_env
|
||||
version = subprocess.run(
|
||||
[path, "--version"],
|
||||
capture_output=True, text=True, timeout=5,
|
||||
env=_cua_driver_env(),
|
||||
).stdout.strip()
|
||||
except Exception:
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -582,6 +582,22 @@ def _cua_driver_cmd() -> str:
|
|||
return os.environ.get("HERMES_CUA_DRIVER_CMD", "").strip() or "cua-driver"
|
||||
|
||||
|
||||
def _cua_driver_env() -> dict:
|
||||
"""cua-driver child env with the Hermes telemetry policy applied.
|
||||
|
||||
Delegates to ``cua_backend.cua_driver_child_env`` (telemetry disabled by
|
||||
default; user opt-in via ``computer_use.cua_telemetry``). Falls back to the
|
||||
current environment if the helper can't be imported, so install/status
|
||||
never break on a telemetry-helper error.
|
||||
"""
|
||||
try:
|
||||
from tools.computer_use.cua_backend import cua_driver_child_env
|
||||
|
||||
return cua_driver_child_env()
|
||||
except Exception:
|
||||
return dict(os.environ)
|
||||
|
||||
|
||||
def _pip_install(
|
||||
args: List[str],
|
||||
*,
|
||||
|
|
@ -804,7 +820,7 @@ def install_cua_driver(upgrade: bool = False) -> bool:
|
|||
try:
|
||||
version = subprocess.run(
|
||||
[driver_cmd, "--version"],
|
||||
capture_output=True, text=True, timeout=5,
|
||||
capture_output=True, text=True, timeout=5, env=_cua_driver_env(),
|
||||
).stdout.strip()
|
||||
_print_success(f" {driver_cmd} already installed: {version or 'unknown version'}")
|
||||
except Exception:
|
||||
|
|
@ -850,7 +866,7 @@ def install_cua_driver(upgrade: bool = False) -> bool:
|
|||
try:
|
||||
before = subprocess.run(
|
||||
[driver_cmd, "--version"],
|
||||
capture_output=True, text=True, timeout=5,
|
||||
capture_output=True, text=True, timeout=5, env=_cua_driver_env(),
|
||||
).stdout.strip()
|
||||
except Exception:
|
||||
before = ""
|
||||
|
|
@ -862,7 +878,7 @@ def install_cua_driver(upgrade: bool = False) -> bool:
|
|||
try:
|
||||
after = subprocess.run(
|
||||
[driver_cmd, "--version"],
|
||||
capture_output=True, text=True, timeout=5,
|
||||
capture_output=True, text=True, timeout=5, env=_cua_driver_env(),
|
||||
).stdout.strip()
|
||||
if after and after != before:
|
||||
_print_success(f" {driver_cmd} upgraded: {before} → {after}")
|
||||
|
|
@ -921,7 +937,7 @@ def _run_cua_driver_installer(label: str = "Installing", verbose: bool = True) -
|
|||
_print_info(f" {label} cua-driver...")
|
||||
driver_cmd = _cua_driver_cmd()
|
||||
try:
|
||||
result = subprocess.run(install_cmd, shell=use_shell, timeout=300)
|
||||
result = subprocess.run(install_cmd, shell=use_shell, timeout=300, env=_cua_driver_env())
|
||||
if result.returncode == 0 and shutil.which(driver_cmd):
|
||||
if verbose:
|
||||
_print_success(f" {driver_cmd} installed.")
|
||||
|
|
|
|||
|
|
@ -623,6 +623,10 @@ _CATEGORY_MERGE: Dict[str, str] = {
|
|||
# with the other messaging-platform config (discord) so it isn't an
|
||||
# orphan tab of one field.
|
||||
"telegram": "discord",
|
||||
# `computer_use.cua_telemetry` is the only schema-surfaced computer_use
|
||||
# field — fold it into the agent tab rather than spawning a one-field
|
||||
# orphan category.
|
||||
"computer_use": "agent",
|
||||
}
|
||||
|
||||
# Display order for tabs — unlisted categories sort alphabetically after these.
|
||||
|
|
|
|||
80
tests/computer_use/test_cua_telemetry.py
Normal file
80
tests/computer_use/test_cua_telemetry.py
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
"""Tests for the cua-driver telemetry opt-in policy.
|
||||
|
||||
cua-driver ships anonymous PostHog telemetry ENABLED by default upstream.
|
||||
Hermes disables it unless the user opts in via
|
||||
``computer_use.cua_telemetry: true``. The policy is applied by injecting
|
||||
``CUA_DRIVER_RS_TELEMETRY_ENABLED=0`` into every cua-driver child env.
|
||||
|
||||
These assert the behavior contract (default disables, opt-in leaves the var
|
||||
untouched, config failure fails safe toward disabled), not specific config
|
||||
snapshots.
|
||||
"""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from tools.computer_use import cua_backend
|
||||
|
||||
|
||||
_VAR = "CUA_DRIVER_RS_TELEMETRY_ENABLED"
|
||||
|
||||
|
||||
class TestTelemetryDisabledFlag:
|
||||
def test_default_config_disables(self):
|
||||
# cua_telemetry absent / False => telemetry disabled.
|
||||
with patch("hermes_cli.config.load_config", return_value={}):
|
||||
assert cua_backend._cua_telemetry_disabled() is True
|
||||
|
||||
def test_explicit_false_disables(self):
|
||||
with patch("hermes_cli.config.load_config",
|
||||
return_value={"computer_use": {"cua_telemetry": False}}):
|
||||
assert cua_backend._cua_telemetry_disabled() is True
|
||||
|
||||
def test_opt_in_true_does_not_disable(self):
|
||||
with patch("hermes_cli.config.load_config",
|
||||
return_value={"computer_use": {"cua_telemetry": True}}):
|
||||
assert cua_backend._cua_telemetry_disabled() is False
|
||||
|
||||
def test_config_load_failure_fails_safe(self):
|
||||
# Unreadable config => default to disabling telemetry (privacy-safe).
|
||||
with patch("hermes_cli.config.load_config", side_effect=RuntimeError("boom")):
|
||||
assert cua_backend._cua_telemetry_disabled() is True
|
||||
|
||||
def test_missing_section_disables(self):
|
||||
with patch("hermes_cli.config.load_config", return_value={"other": {}}):
|
||||
assert cua_backend._cua_telemetry_disabled() is True
|
||||
|
||||
|
||||
class TestChildEnv:
|
||||
def test_disabled_injects_var_zero(self):
|
||||
with patch.object(cua_backend, "_cua_telemetry_disabled", return_value=True):
|
||||
env = cua_backend.cua_driver_child_env({"PATH": "/usr/bin"})
|
||||
assert env[_VAR] == "0"
|
||||
# base env is preserved
|
||||
assert env["PATH"] == "/usr/bin"
|
||||
|
||||
def test_opt_in_leaves_var_untouched(self):
|
||||
# When the user opts in, we must NOT set the var — the driver uses its
|
||||
# own default. If the base env already has a value, it is preserved.
|
||||
with patch.object(cua_backend, "_cua_telemetry_disabled", return_value=False):
|
||||
env = cua_backend.cua_driver_child_env({"PATH": "/usr/bin"})
|
||||
assert _VAR not in env
|
||||
|
||||
def test_opt_in_preserves_user_set_var(self):
|
||||
with patch.object(cua_backend, "_cua_telemetry_disabled", return_value=False):
|
||||
env = cua_backend.cua_driver_child_env({_VAR: "1", "PATH": "/usr/bin"})
|
||||
# user opted in and explicitly set it — don't clobber.
|
||||
assert env[_VAR] == "1"
|
||||
|
||||
def test_disabled_overrides_inherited_enabled(self):
|
||||
# Even if the parent process had telemetry enabled, the default policy
|
||||
# forces it off in the child.
|
||||
with patch.object(cua_backend, "_cua_telemetry_disabled", return_value=True):
|
||||
env = cua_backend.cua_driver_child_env({_VAR: "1"})
|
||||
assert env[_VAR] == "0"
|
||||
|
||||
def test_defaults_to_os_environ_when_no_base(self):
|
||||
with patch.object(cua_backend, "_cua_telemetry_disabled", return_value=True), \
|
||||
patch.dict("os.environ", {"SOME_MARKER": "yes"}, clear=False):
|
||||
env = cua_backend.cua_driver_child_env()
|
||||
assert env.get("SOME_MARKER") == "yes"
|
||||
assert env[_VAR] == "0"
|
||||
|
|
@ -78,6 +78,45 @@ _CUA_DRIVER_ARGS = ["mcp"] # stdio MCP transport (fallback when the
|
|||
# driver doesn't expose `manifest` — see
|
||||
# `_resolve_mcp_invocation` below)
|
||||
|
||||
# Env var cua-driver reads to gate its anonymous usage telemetry (PostHog).
|
||||
# Setting it to "0" disables telemetry; absence => the binary's own default
|
||||
# (telemetry ON upstream).
|
||||
_CUA_TELEMETRY_ENV_VAR = "CUA_DRIVER_RS_TELEMETRY_ENABLED"
|
||||
|
||||
|
||||
def _cua_telemetry_disabled() -> bool:
|
||||
"""True when Hermes should disable cua-driver telemetry for this user.
|
||||
|
||||
Reads ``computer_use.cua_telemetry`` from config.yaml. Default is False
|
||||
(telemetry off). Any failure to read config fails SAFE — toward the
|
||||
privacy-preserving default of telemetry disabled.
|
||||
"""
|
||||
try:
|
||||
from hermes_cli.config import load_config
|
||||
|
||||
cfg = load_config() or {}
|
||||
cu = cfg.get("computer_use") or {}
|
||||
# opt-in flag: True => user wants telemetry => do NOT disable.
|
||||
return not bool(cu.get("cua_telemetry", False))
|
||||
except Exception:
|
||||
# Config unreadable — default to disabling telemetry (fail safe).
|
||||
return True
|
||||
|
||||
|
||||
def cua_driver_child_env(base_env: Optional[Dict[str, str]] = None) -> Dict[str, str]:
|
||||
"""Return the environment dict for spawning cua-driver.
|
||||
|
||||
Starts from ``base_env`` (defaults to ``os.environ``) and, when telemetry
|
||||
is disabled (the default), injects ``CUA_DRIVER_RS_TELEMETRY_ENABLED=0``.
|
||||
When the user has opted in, the var is left untouched so cua-driver uses
|
||||
its own default. Used by every cua-driver spawn site (MCP backend, status,
|
||||
doctor, install) so the policy is applied consistently.
|
||||
"""
|
||||
env = dict(base_env if base_env is not None else os.environ)
|
||||
if _cua_telemetry_disabled():
|
||||
env[_CUA_TELEMETRY_ENV_VAR] = "0"
|
||||
return env
|
||||
|
||||
|
||||
def _resolve_mcp_invocation(
|
||||
driver_cmd: str,
|
||||
|
|
@ -176,6 +215,7 @@ def cua_driver_update_check(*, timeout: float = 8.0) -> Optional[Dict[str, Any]]
|
|||
# stdin-reading mode rather than erroring — DEVNULL gives them EOF
|
||||
# so they exit fast instead of blocking until the timeout.
|
||||
stdin=subprocess.DEVNULL,
|
||||
env=cua_driver_child_env(),
|
||||
)
|
||||
except Exception:
|
||||
return None
|
||||
|
|
@ -523,7 +563,9 @@ class _CuaDriverSession:
|
|||
params = StdioServerParameters(
|
||||
command=command,
|
||||
args=args,
|
||||
env=_sanitize_subprocess_env(dict(os.environ)),
|
||||
# Apply the telemetry policy first (default: disabled), then
|
||||
# sanitize Hermes-managed secrets out of the child env.
|
||||
env=_sanitize_subprocess_env(cua_driver_child_env()),
|
||||
)
|
||||
|
||||
async with stdio_client(params) as (read, write):
|
||||
|
|
|
|||
|
|
@ -37,6 +37,21 @@ _OVERALL_GLYPH = {
|
|||
}
|
||||
|
||||
|
||||
def _cua_child_env() -> Dict[str, str]:
|
||||
"""cua-driver child env with the Hermes telemetry policy applied.
|
||||
|
||||
Delegates to ``cua_backend.cua_driver_child_env`` (telemetry disabled by
|
||||
default unless the user opts in). Falls back to the current environment
|
||||
if that import fails, so doctor never breaks on a telemetry-helper error.
|
||||
"""
|
||||
try:
|
||||
from tools.computer_use.cua_backend import cua_driver_child_env
|
||||
|
||||
return cua_driver_child_env()
|
||||
except Exception:
|
||||
return dict(os.environ)
|
||||
|
||||
|
||||
def _drive_health_report(
|
||||
binary: str,
|
||||
*,
|
||||
|
|
@ -72,6 +87,7 @@ def _drive_health_report(
|
|||
encoding="utf-8",
|
||||
errors="replace",
|
||||
bufsize=1,
|
||||
env=_cua_child_env(),
|
||||
)
|
||||
try:
|
||||
# 1. initialize
|
||||
|
|
|
|||
|
|
@ -288,6 +288,25 @@ Swap the backend entirely (for testing):
|
|||
HERMES_COMPUTER_USE_BACKEND=noop # records calls, no side effects
|
||||
```
|
||||
|
||||
### Telemetry
|
||||
|
||||
cua-driver ships with anonymous usage telemetry (PostHog) enabled by default
|
||||
upstream. **Hermes disables it for you** — on every cua-driver invocation
|
||||
(the MCP backend, `status`, `doctor`, and install) Hermes sets
|
||||
`CUA_DRIVER_RS_TELEMETRY_ENABLED=0` in the driver's environment.
|
||||
|
||||
To opt back in (let cua-driver use its own default and send telemetry), set
|
||||
this in `config.yaml`:
|
||||
|
||||
```yaml
|
||||
computer_use:
|
||||
cua_telemetry: true # default: false (telemetry off)
|
||||
```
|
||||
|
||||
When it's on, `hermes computer-use doctor` reports `telemetry: enabled`;
|
||||
when off (the default), it reports `telemetry: disabled via
|
||||
CUA_DRIVER_RS_TELEMETRY_ENABLED`.
|
||||
|
||||
## Testing against a local cua-driver build
|
||||
|
||||
When you're developing cua-driver itself — or want to test an
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue