refactor(telemetry): drop policy.resolve(); read config directly

policy.resolve() / TelemetryDecision was a read-only projection used only by
`hermes telemetry status` for display. The actual behavior gates already read
telemetry.* straight from config: the emitter (whether to write) and the plugin
loader (whether to auto-load) each call .get("local", True) on the loaded config,
never through policy.

Make config the single chokepoint the status command reads too: it now resolves
local/allow_aggregate/consent_state inline from the loaded config, the same way
the other gates do. policy.py keeps only what config can't express on its own —
the consent constants, ensure_install_id(), and may_upload_aggregate(config) as a
pure function (the gate a future uploader must consult). resolve() and the
TelemetryDecision dataclass are removed; policy.py drops 107 -> 70 lines.

No behavior change: status renders identically, and the default-on local plane is
still defaulted in DEFAULT_CONFIG plus a fail-safe .get(..., True) at each gate.
This commit is contained in:
emozilla 2026-06-25 21:32:30 -04:00 committed by Victor Kyriazakos
parent 3e28eaccde
commit e829d03fec
5 changed files with 48 additions and 96 deletions

View file

@ -8,8 +8,8 @@ raises into a model/tool call (the hot-path invariant).
Events record the observed model ids, provider names, and tool names. ``metrics``
derives rollups for /usage and /insights; ``rollup`` builds the per-run summaries shown
by ``hermes telemetry preview``. ``redaction`` + ``exporter_bulk`` + ``otlp_exporter``
handle export to an operator-chosen destination. ``policy`` holds the consent state
machine for the opt-in aggregate plane (no uploader ships).
handle export to an operator-chosen destination. ``policy`` holds the consent
constants and the aggregate upload gate (no uploader ships).
"""
from __future__ import annotations

View file

@ -1,49 +1,32 @@
"""Telemetry consent posture and the aggregate-plane gate.
Consent is a single field, ``telemetry.consent_state``:
Consent is a single config field, ``telemetry.consent_state``:
* "unknown" no choice recorded; never uploads (the default).
* "local" declined the aggregate plane; local plane only.
* "aggregate" opted in to the aggregate plane.
The config file is the source of truth: set ``telemetry.consent_state`` with
``hermes config set`` (or a managed-scope pin). There is no separate boolean mirror
a single field cannot drift out of sync with itself, so a stray value can't
accidentally imply consent.
``hermes config set`` (or a managed-scope pin). Callers that gate behavior read
``telemetry.*`` directly from config; this module only provides the consent
constants, the install-id helper, and the upload gate a future uploader must
consult.
``allow_aggregate`` is the hard gate. An administrator pins
``telemetry.allow_aggregate: false`` through the managed-scope layer
(``/etc/hermes/config.yaml``), which takes precedence over the user's config; when it
is false, the aggregate plane is off regardless of ``consent_state``.
This module makes the decisions; it performs no I/O and contains no uploader. A future
uploader must call :func:`may_upload_aggregate` at its boundary.
(``/etc/hermes/config.yaml``), which takes precedence over the user's config; when
it is false, the aggregate plane is off regardless of ``consent_state``.
"""
from __future__ import annotations
import uuid
from dataclasses import dataclass
from typing import Any, Dict
CONSENT_UNKNOWN = "unknown"
CONSENT_LOCAL = "local"
CONSENT_AGGREGATE = "aggregate"
_VALID_STATES = {CONSENT_UNKNOWN, CONSENT_LOCAL, CONSENT_AGGREGATE}
@dataclass(slots=True)
class TelemetryDecision:
"""The resolved telemetry posture for the current process."""
local_enabled: bool
aggregate_enabled: bool
consent_state: str
install_id: str
allow_aggregate: bool
def may_upload_aggregate(self) -> bool:
"""The single gate the uploader must consult before any network send."""
return self.allow_aggregate and self.consent_state == CONSENT_AGGREGATE
VALID_CONSENT_STATES = {CONSENT_UNKNOWN, CONSENT_LOCAL, CONSENT_AGGREGATE}
def _telemetry_cfg(config: Dict[str, Any]) -> Dict[str, Any]:
@ -65,43 +48,23 @@ def ensure_install_id(config: Dict[str, Any]) -> str:
return str(uuid.uuid4())
def resolve(config: Dict[str, Any]) -> TelemetryDecision:
"""Resolve the effective telemetry posture from config.
def may_upload_aggregate(config: Dict[str, Any]) -> bool:
"""Whether the aggregate plane may upload — the gate a future uploader consults.
``consent_state`` is the single source of truth for the aggregate opt-in.
``allow_aggregate`` (admin-pinnable via managed scope) hard-disables the aggregate
plane regardless of consent.
True only when the admin hard gate allows it AND the user has opted in via
``telemetry.consent_state``.
"""
tel = _telemetry_cfg(config)
local_enabled = bool(tel.get("local", True))
allow_aggregate = bool(tel.get("allow_aggregate", True))
state = tel.get("consent_state", CONSENT_UNKNOWN)
if state not in _VALID_STATES:
state = CONSENT_UNKNOWN
aggregate_enabled = allow_aggregate and state == CONSENT_AGGREGATE
return TelemetryDecision(
local_enabled=local_enabled,
aggregate_enabled=aggregate_enabled,
consent_state=state,
install_id=ensure_install_id(config),
allow_aggregate=allow_aggregate,
)
def may_upload_aggregate(config: Dict[str, Any]) -> bool:
"""Convenience gate for the uploader boundary."""
return resolve(config).may_upload_aggregate()
return allow_aggregate and state == CONSENT_AGGREGATE
__all__ = [
"CONSENT_UNKNOWN",
"CONSENT_LOCAL",
"CONSENT_AGGREGATE",
"TelemetryDecision",
"resolve",
"VALID_CONSENT_STATES",
"may_upload_aggregate",
"ensure_install_id",
]

View file

@ -14322,25 +14322,32 @@ def cmd_telemetry(args):
action = getattr(args, "telemetry_action", None) or "status"
config = load_config()
decision = policy.resolve(config)
tel = config.get("telemetry") if isinstance(config.get("telemetry"), dict) else {}
local_enabled = bool(tel.get("local", True))
allow_aggregate = bool(tel.get("allow_aggregate", True))
consent_state = tel.get("consent_state", policy.CONSENT_UNKNOWN)
if consent_state not in policy.VALID_CONSENT_STATES:
consent_state = policy.CONSENT_UNKNOWN
aggregate_enabled = allow_aggregate and consent_state == policy.CONSENT_AGGREGATE
install_id = policy.ensure_install_id(config)
def _persist_install_id():
# Make sure a minted id is written back so it stays stable.
config.setdefault("telemetry", {})["install_id"] = decision.install_id
config.setdefault("telemetry", {})["install_id"] = install_id
save_config(config)
if action == "status":
print("Telemetry status")
print("" * 56)
print(f" Local plane: {'on' if decision.local_enabled else 'off'} "
print(f" Local plane: {'on' if local_enabled else 'off'} "
f"(telemetry.local)")
print(f" Aggregate plane: {'on' if decision.aggregate_enabled else 'off'} "
f"(opt-in; consent_state={decision.consent_state})")
if decision.consent_state != policy.CONSENT_AGGREGATE and decision.allow_aggregate:
print(f" Aggregate plane: {'on' if aggregate_enabled else 'off'} "
f"(opt-in; consent_state={consent_state})")
if consent_state != policy.CONSENT_AGGREGATE and allow_aggregate:
print(" opt in: hermes config set telemetry.consent_state aggregate")
if not decision.allow_aggregate:
if not allow_aggregate:
print(" ⚠ allow_aggregate is false (egress hard-disabled)")
print(f" Install id: {decision.install_id}")
print(f" Install id: {install_id}")
print(" Upload: DISABLED — no server yet. Aggregate is computed "
"locally only.")
print()
@ -14393,7 +14400,7 @@ def cmd_telemetry(args):
_persist_install_id()
since_ns = int((time.time() - args.days * 86400) * 1e9)
events = rollup.build_aggregate_events(
install_id=decision.install_id, since_ns=since_ns
install_id=install_id, since_ns=since_ns
)
summary = rollup.summarize(events)
print("Telemetry preview — computed locally, NOT uploaded")

View file

@ -83,7 +83,7 @@ def test_allow_aggregate_pin_blocks_opt_in(home):
"""A managed allow_aggregate:false pin overrides a consent_state opt-in.
Consent is set in config (as a user or managed-scope pin would); the hard gate
still wins, so the aggregate plane resolves off and may_upload stays false.
still wins, so may_upload stays false.
"""
from hermes_cli.config import load_config, save_config
from agent.telemetry import policy
@ -92,7 +92,4 @@ def test_allow_aggregate_pin_blocks_opt_in(home):
tel["consent_state"] = "aggregate"
tel["allow_aggregate"] = False
save_config(c)
d = policy.resolve(load_config())
assert d.allow_aggregate is False
assert d.aggregate_enabled is False
assert d.may_upload_aggregate() is False
assert policy.may_upload_aggregate(load_config()) is False

View file

@ -1,8 +1,9 @@
"""Consent posture + org-policy enforcement tests.
"""Consent gate tests.
Consent is a single field (``telemetry.consent_state``); the aggregate opt-in is
expressed by setting it to ``"aggregate"`` (via ``hermes config set`` or a managed-scope
pin). ``allow_aggregate`` is the hard gate.
Consent is a single config field (``telemetry.consent_state``); the aggregate opt-in
is expressed by setting it to ``"aggregate"`` (via ``hermes config set`` or a
managed-scope pin). ``allow_aggregate`` is the hard gate. ``policy.may_upload_aggregate``
is the gate a future uploader must consult.
"""
from __future__ import annotations
@ -14,43 +15,27 @@ def _cfg(**telemetry):
return {"telemetry": telemetry}
def test_default_posture_is_local_only():
d = policy.resolve(_cfg(local=True, consent_state="unknown"))
assert d.local_enabled is True
assert d.aggregate_enabled is False
assert d.may_upload_aggregate() is False
def test_default_posture_never_uploads():
# No consent recorded → unknown → never uploads.
assert policy.may_upload_aggregate(_cfg(local=True, consent_state="unknown")) is False
def test_unknown_consent_never_uploads():
# A headless box with no choice recorded: stays unknown, never uploads.
d = policy.resolve(_cfg(local=True, consent_state="unknown"))
assert d.may_upload_aggregate() is False
def test_missing_telemetry_block_never_uploads():
assert policy.may_upload_aggregate({}) is False
def test_opted_in_uploads():
d = policy.resolve(_cfg(local=True, consent_state="aggregate"))
assert d.aggregate_enabled is True
assert d.may_upload_aggregate() is True
assert policy.may_upload_aggregate(_cfg(consent_state="aggregate")) is True
def test_declined_does_not_upload():
d = policy.resolve(_cfg(local=True, consent_state="local"))
assert d.may_upload_aggregate() is False
assert policy.may_upload_aggregate(_cfg(consent_state="local")) is False
def test_allow_aggregate_false_overrides_opt_in():
# An admin pins telemetry.allow_aggregate: false via managed scope.
cfg = _cfg(local=True, consent_state="aggregate", allow_aggregate=False)
d = policy.resolve(cfg)
assert d.allow_aggregate is False
assert d.aggregate_enabled is False
assert d.may_upload_aggregate() is False # the hard gate wins
def test_invalid_consent_state_treated_as_unknown():
d = policy.resolve(_cfg(local=True, consent_state="bogus"))
assert d.consent_state == "unknown"
assert d.may_upload_aggregate() is False
cfg = _cfg(consent_state="aggregate", allow_aggregate=False)
assert policy.may_upload_aggregate(cfg) is False # the hard gate wins
def test_install_id_minted_when_empty_and_stable_when_set():