mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
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:
parent
3e28eaccde
commit
e829d03fec
5 changed files with 48 additions and 96 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue