mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
refactor(telemetry): drop "plane" terminology
Rename the telemetry tiers away from the borrowed control-plane/data-plane jargon to plain language, across code, CLI output, config, and docs: - "local plane" -> "local telemetry" - "aggregate plane" -> "aggregate metrics" - "trajectories plane" -> "trajectories" / "telemetry.trajectories" - "three planes with a hard wall" -> "three settings, isolated from each other" User-facing `hermes telemetry status` now reads "Local telemetry: on" / "Aggregate metrics: off" / "Content export: off (trajectories disabled)". The OTLP resource attribute key telemetry.plane is renamed to telemetry.scope (wire-level identifier; nothing consumes it yet). No behavior change — wording only. Status renders identically apart from the labels; tests updated to match the new strings.
This commit is contained in:
parent
e829d03fec
commit
72b7af73bd
17 changed files with 105 additions and 103 deletions
|
|
@ -181,7 +181,7 @@ class InsightsEngine:
|
|||
}
|
||||
|
||||
# =========================================================================
|
||||
# Telemetry (observability) — from the tel_* tables (local plane)
|
||||
# Telemetry (observability) — from the tel_* tables (local telemetry)
|
||||
# =========================================================================
|
||||
|
||||
def _compute_telemetry(self, cutoff: float) -> Dict[str, Any]:
|
||||
|
|
@ -1067,7 +1067,7 @@ class InsightsEngine:
|
|||
lines.append(f" {ts['label']:<20} {ts['value']:<18} ({ts['date']}, {ts['session_id']})")
|
||||
lines.append("")
|
||||
|
||||
# Telemetry / observability (local plane) — only when data exists
|
||||
# Telemetry / observability (local telemetry) — only when data exists
|
||||
tel = report.get("telemetry") or {}
|
||||
if tel:
|
||||
self._append_telemetry_section(lines, tel)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""Local-plane telemetry emitter: fire-and-forget queue + background writer.
|
||||
"""Local telemetry emitter: fire-and-forget queue + background writer.
|
||||
|
||||
The emitter is the single seam between instrumentation (the telemetry plugin's hook
|
||||
callbacks) and durable storage. Its contract is the hot-path invariant:
|
||||
|
|
@ -16,7 +16,7 @@ Mechanism:
|
|||
* The writer uses its own sqlite connection to state.db, separate from SessionDB,
|
||||
so telemetry writes never contend with or corrupt session writes.
|
||||
|
||||
Local plane only. Nothing here uploads anywhere.
|
||||
Local telemetry only. Nothing here uploads anywhere.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""Typed local-plane telemetry events.
|
||||
"""Typed local telemetry events.
|
||||
|
||||
These dataclasses are the rows written to the local JSONL log and the ``tel_*``
|
||||
SQLite tables. They record the values observed for each run — model id, provider, tool
|
||||
|
|
@ -11,7 +11,7 @@ import time
|
|||
from dataclasses import dataclass, field, asdict
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
# ── local-plane events (real values) ────────────────────────────────────────
|
||||
# ── local telemetry events (real values) ────────────────────────────────────
|
||||
|
||||
|
||||
def _now_ns() -> int:
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Two data domains, both written to an operator-chosen destination:
|
||||
|
||||
* Telemetry: the tel_* rows + events.jsonl (structural observability).
|
||||
* Content (opt-in via the trajectories plane): sessions + messages, with every
|
||||
* Content (opt-in via telemetry.trajectories): sessions + messages, with every
|
||||
content field (message body, reasoning, raw tool-call args) passed through the
|
||||
redaction pipeline (secrets always stripped; PII per content_redaction).
|
||||
|
||||
|
|
@ -103,10 +103,10 @@ def export(
|
|||
) -> Dict[str, int]:
|
||||
"""Write telemetry (+ optional content) to ``out``. Returns counts.
|
||||
|
||||
``include_content`` is honored only when the trajectories plane is enabled in
|
||||
``include_content`` is honored only when telemetry.trajectories is enabled in
|
||||
``config``; otherwise content is forced off and only structural data is written.
|
||||
"""
|
||||
# Trajectories gate: a flag cannot override the consent plane.
|
||||
# Trajectories gate: a flag cannot override the config setting.
|
||||
content_allowed = include_content and redaction.content_export_enabled(config)
|
||||
counts = {"telemetry": 0, "sessions": 0, "content_included": int(content_allowed)}
|
||||
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ def workflow_summary(
|
|||
*,
|
||||
conn: Optional[sqlite3.Connection] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Run-level counters + duration percentiles (local plane, exact)."""
|
||||
"""Run-level counters + duration percentiles (local telemetry, exact)."""
|
||||
with _cursor(conn, db_path) as c:
|
||||
where = _since_clause(since_ns)
|
||||
total = c.execute(f"SELECT COUNT(*) n FROM tel_runs{where}").fetchone()["n"]
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ Notes:
|
|||
and is fail-isolated, so an export error cannot affect a run.
|
||||
|
||||
Spans carry structural telemetry by default. Message content is included only when the
|
||||
trajectories plane is enabled, and always passes through the export redaction pipeline.
|
||||
trajectories is enabled, and always passes through the export redaction pipeline.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -119,7 +119,7 @@ def _make_provider(config: Dict[str, Any]):
|
|||
sdk = _require_sdk()
|
||||
resource = sdk["Resource"].create({
|
||||
"service.name": "hermes-agent",
|
||||
"telemetry.plane": "local", # never aggregate
|
||||
"telemetry.scope": "local", # never aggregate metrics
|
||||
})
|
||||
provider = sdk["TracerProvider"](resource=resource)
|
||||
processor = sdk["BatchSpanProcessor"](build_exporter(config))
|
||||
|
|
@ -129,7 +129,7 @@ def _make_provider(config: Dict[str, Any]):
|
|||
|
||||
# ── event -> span attribute mapping (real values) ───────────────────────────
|
||||
def _span_attrs(ev: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Span attributes for an event — the real recorded values (local plane)."""
|
||||
"""Span attributes for an event — the real recorded values (local telemetry)."""
|
||||
kind = ev.get("event")
|
||||
attrs: Dict[str, Any] = {"hermes.event": kind or "unknown"}
|
||||
keep_by_kind = {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
"""Telemetry consent posture and the aggregate-plane gate.
|
||||
"""Telemetry consent posture and the aggregate-metrics gate.
|
||||
|
||||
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.
|
||||
* "local" — declined aggregate metrics; local telemetry only.
|
||||
* "aggregate" — opted in to aggregate metrics.
|
||||
|
||||
The config file is the source of truth: set ``telemetry.consent_state`` with
|
||||
``hermes config set`` (or a managed-scope pin). Callers that gate behavior read
|
||||
|
|
@ -15,7 +15,7 @@ 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``.
|
||||
it is false, aggregate metrics are off regardless of ``consent_state``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -49,7 +49,7 @@ def ensure_install_id(config: Dict[str, Any]) -> str:
|
|||
|
||||
|
||||
def may_upload_aggregate(config: Dict[str, Any]) -> bool:
|
||||
"""Whether the aggregate plane may upload — the gate a future uploader consults.
|
||||
"""Whether aggregate metrics may upload — the gate a future uploader consults.
|
||||
|
||||
True only when the admin hard gate allows it AND the user has opted in via
|
||||
``telemetry.consent_state``.
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ Two independent controls:
|
|||
disables this. Wraps ``agent/redact.py::redact_sensitive_text(force=True)``.
|
||||
|
||||
* Whether message bodies, reasoning, and raw tool arguments are exportable at all is
|
||||
governed by the trajectories plane (``telemetry.trajectories.enabled``, default
|
||||
governed by the trajectories setting (``telemetry.trajectories.enabled``, default
|
||||
off, admin-pinnable), not by a redaction mode. With trajectories off, content is
|
||||
dropped. With it on, content is exportable and ``content_redaction`` (none|pii)
|
||||
controls how much is scrubbed; secrets are still always stripped.
|
||||
|
|
@ -62,7 +62,7 @@ def redact_for_export(
|
|||
"""Redact a single content string for export.
|
||||
|
||||
Secrets are ALWAYS stripped. Then PII is stripped when content_mode is 'pii'.
|
||||
Callers gate *whether content is exported at all* via the trajectories plane
|
||||
Callers gate *whether content is exported at all* via telemetry.trajectories
|
||||
(see ``content_export_enabled``); this function only scrubs content that the
|
||||
caller has already decided to export.
|
||||
"""
|
||||
|
|
@ -75,7 +75,7 @@ def redact_for_export(
|
|||
|
||||
|
||||
def content_export_enabled(config: Optional[Dict[str, Any]]) -> bool:
|
||||
"""True only when the trajectories plane is explicitly enabled.
|
||||
"""True only when telemetry.trajectories is explicitly enabled.
|
||||
|
||||
This is the consent gate for exporting message bodies / reasoning / raw tool
|
||||
args. Default off. Admin-pinnable via managed scope (telemetry.trajectories.enabled).
|
||||
|
|
|
|||
|
|
@ -1417,13 +1417,13 @@ display:
|
|||
# =============================================================================
|
||||
# Telemetry & Observability
|
||||
# =============================================================================
|
||||
# Three planes with a hard wall between them:
|
||||
# Three settings, isolated from each other:
|
||||
# local — full-fidelity observability you own. Default ON.
|
||||
# aggregate — opt-in metadata to Nous (no uploader ships yet). Default OFF.
|
||||
# trajectories — content trajectories for training. Separate consent (later).
|
||||
#
|
||||
# The local plane records real values (actual models, providers, tool names) —
|
||||
# your own data, on your machine. The aggregate plane is opt-in and has no
|
||||
# Local telemetry records real values (actual models, providers, tool names) —
|
||||
# your own data, on your machine. Aggregate metrics are opt-in and have no
|
||||
# uploader today; if one ships it would summarize at that egress boundary.
|
||||
#
|
||||
# Enterprise locking: any telemetry.* key can be pinned by an administrator via
|
||||
|
|
@ -1431,16 +1431,16 @@ display:
|
|||
# value. To hard-forbid egress on a locked-down deployment, pin
|
||||
# `telemetry.allow_aggregate: false` there.
|
||||
# telemetry:
|
||||
# # Local plane: event log + SQLite index in state.db. Never leaves your
|
||||
# # machine unless you export it or opt into the aggregate plane.
|
||||
# # Local telemetry: event log + SQLite index in state.db. Never leaves your
|
||||
# # machine unless you export it or opt into aggregate metrics.
|
||||
# local: true
|
||||
# # Hard gate for the aggregate plane. When false, the aggregate plane is off
|
||||
# # Hard gate for aggregate metrics. When false, aggregate metrics are off
|
||||
# # regardless of consent_state. Pin false via managed scope to forbid egress.
|
||||
# allow_aggregate: true
|
||||
# # Aggregate-plane consent (the opt-in). No uploader ships yet.
|
||||
# # Aggregate-metrics consent (the opt-in). No uploader ships yet.
|
||||
# # unknown (no choice — never uploads) | local (declined) | aggregate (opted in)
|
||||
# consent_state: unknown
|
||||
# # Stable install id (aggregate plane only). Empty = mint on first use;
|
||||
# # Stable install id (aggregate metrics only). Empty = mint on first use;
|
||||
# # clear it to rotate.
|
||||
# install_id: ""
|
||||
# # Local event-log retention before rotation (days).
|
||||
|
|
|
|||
|
|
@ -6,57 +6,57 @@ agent does — workflows, model calls, tool calls, errors — to your own machin
|
|||
stack. It is private by default and never sends your data to Nous unless you explicitly
|
||||
opt in to anonymous aggregate metrics.
|
||||
|
||||
This page explains the whole feature: the three planes, what's captured, the
|
||||
This page explains the whole feature: the three telemetry settings, what's captured, the
|
||||
`hermes telemetry` commands, and how an enterprise streams or exports all of its data
|
||||
to its own infrastructure.
|
||||
|
||||
> Looking for the **plugin hook contract** (how to write your own observer plugin)?
|
||||
> That's in [`README.md`](./README.md). This page is about the built-in telemetry
|
||||
> plane and its CLI.
|
||||
> system and its CLI.
|
||||
|
||||
## The three planes
|
||||
## The three settings
|
||||
|
||||
Telemetry is organized into three planes with a hard wall between them:
|
||||
Telemetry has three settings, isolated from each other:
|
||||
|
||||
| Plane | What it holds | Default | Destination |
|
||||
| Setting | What it holds | Default | Destination |
|
||||
| --- | --- | --- | --- |
|
||||
| **local** | Full-fidelity observability — runs, model/tool calls (real model & provider names), durations, errors | **on** | your machine only |
|
||||
| **aggregate** | Opt-in metadata to Nous (no uploader ships yet) | **off** (opt-in) | Nous, only if you enable it |
|
||||
| **trajectories** | Full message content / reasoning / raw tool args | **off** (opt-in) | your own export destinations only |
|
||||
|
||||
The local plane is the one you'll use day to day — it records the real values that
|
||||
happened (actual model ids, providers, tool names). The aggregate plane is the only
|
||||
thing that could ever leave for Nous; it is opt-in, default-off, and has no uploader
|
||||
today. The trajectories plane unlocks full-content export to *your own* destinations —
|
||||
it is never wired to Nous.
|
||||
Local telemetry is the one you'll use day to day — it records the real values that
|
||||
happened (actual model ids, providers, tool names). Aggregate metrics are the only
|
||||
thing that could ever leave for Nous; they are opt-in, default-off, and have no uploader
|
||||
today. Trajectories unlock full-content export to *your own* destinations — never wired
|
||||
to Nous.
|
||||
|
||||
## Local plane — always-on observability
|
||||
## Local telemetry — always-on observability
|
||||
|
||||
The local plane is implemented as a bundled `telemetry` plugin that listens to Hermes
|
||||
Local telemetry is implemented as a bundled `telemetry` plugin that listens to Hermes
|
||||
lifecycle hooks (model calls, tool calls, session start/finalize) and writes events to:
|
||||
|
||||
- an append-only JSONL log at `~/.hermes/telemetry/events.jsonl` (the source of truth)
|
||||
- indexed `tel_*` tables in `state.db` (for fast queries and rollups)
|
||||
|
||||
Writes are fire-and-forget on a background thread: telemetry can never block, slow, or
|
||||
fail a model call or tool call. If the local plane is disabled (`telemetry.local: false`)
|
||||
fail a model call or tool call. If local telemetry is disabled (`telemetry.local: false`)
|
||||
the plugin does not load at all.
|
||||
|
||||
### Seeing your local data
|
||||
|
||||
```bash
|
||||
hermes insights # usage report — now includes an "Observability" section
|
||||
hermes telemetry status # planes, consent, export posture, local data volume
|
||||
hermes telemetry status # settings, consent, export posture, local data volume
|
||||
```
|
||||
|
||||
The `insights` Observability section shows workflow counts and success rate, duration
|
||||
p50/p95, tool failure rates by category, provider/model-class mix, and cache hit rate —
|
||||
p50/p95, tool failure rates by category, provider/model mix, and cache hit rate —
|
||||
all computed locally with exact values.
|
||||
|
||||
## `hermes telemetry` commands
|
||||
|
||||
```text
|
||||
hermes telemetry status Show planes, consent state, export posture, local volume
|
||||
hermes telemetry status Show settings, consent state, export posture, local volume
|
||||
hermes telemetry preview Show the aggregate events that would be produced (local)
|
||||
hermes telemetry export Export local telemetry to a file/stream or OTLP endpoint
|
||||
```
|
||||
|
|
@ -65,14 +65,14 @@ Consent and the install id are plain config, not separate verbs — set them wit
|
|||
`hermes config set` (or a managed-scope pin):
|
||||
|
||||
```bash
|
||||
hermes config set telemetry.consent_state aggregate # opt in to the aggregate plane
|
||||
hermes config set telemetry.consent_state local # opt out (local plane stays on)
|
||||
hermes config set telemetry.consent_state aggregate # opt in to aggregate metrics
|
||||
hermes config set telemetry.consent_state local # opt out (local telemetry stays on)
|
||||
hermes config set telemetry.install_id "" # reset the install id (mints a new one)
|
||||
```
|
||||
|
||||
### Aggregate plane (opt-in)
|
||||
### Aggregate metrics (opt-in)
|
||||
|
||||
The aggregate plane is **off by default** and has **no uploader today** — nothing is
|
||||
Aggregate metrics are **off by default** and have **no uploader today** — nothing is
|
||||
sent to Nous. Consent lives in `telemetry.consent_state` (`unknown` / `local` /
|
||||
`aggregate`); setting it to `aggregate` records the opt-in for if/when an uploader ships.
|
||||
If one is built, it would summarize at that egress boundary.
|
||||
|
|
@ -99,11 +99,11 @@ hermes telemetry export --out dump.json --format json --since 7
|
|||
By default the export is **structural** — runs, model/tool-call metadata, session shells
|
||||
with message *counts* but no message bodies.
|
||||
|
||||
### Including content (trajectories plane)
|
||||
### Including content (trajectories)
|
||||
|
||||
To export full message content, enable the trajectories plane. This is a deliberate,
|
||||
separate consent — it's how an enterprise opts into exporting work-product content to its
|
||||
own store:
|
||||
To export full message content, enable trajectories. This is a deliberate, separate
|
||||
consent — it's how an enterprise opts into exporting work-product content to its own
|
||||
store:
|
||||
|
||||
```yaml
|
||||
# config.yaml
|
||||
|
|
@ -117,8 +117,8 @@ telemetry:
|
|||
hermes telemetry export --out full.ndjson --include-content
|
||||
```
|
||||
|
||||
`--include-content` is a no-op unless the trajectories plane is enabled — the consent
|
||||
plane governs, not the flag.
|
||||
`--include-content` is a no-op unless trajectories are enabled — the config setting
|
||||
governs, not the flag.
|
||||
|
||||
### Live streaming to your OpenTelemetry Collector / SIEM (OTLP)
|
||||
|
||||
|
|
@ -137,17 +137,19 @@ telemetry:
|
|||
enabled: true
|
||||
endpoint: "https://collector.your-corp.internal:4318/v1/traces"
|
||||
headers_env: # secrets by reference — env var NAMES, not values
|
||||
Authorization: CORP_OTLP_TOKEN
|
||||
Authorization: MY_OTLP_TOKEN_ENVVAR
|
||||
```
|
||||
|
||||
Set the referenced environment variable, then run the export:
|
||||
|
||||
```bash
|
||||
export CORP_OTLP_TOKEN="..." # the actual token lives in the environment
|
||||
hermes telemetry export --otlp # drain current telemetry to your collector
|
||||
```
|
||||
|
||||
Span attributes are structural by default. For authentication, the config holds the
|
||||
*name* of an environment variable rather than the secret itself; the value is read at
|
||||
export time and is never written to config or logged.
|
||||
The token value lives only in the environment variable named by `headers_env`. Span
|
||||
attributes are structural by default. The config holds the *name* of an environment
|
||||
variable rather than the secret itself; the value is read at export time and is never
|
||||
written to config or logged.
|
||||
|
||||
## Redaction
|
||||
|
||||
|
|
@ -166,8 +168,8 @@ fails closed: if the redactor can't run, the raw string is not emitted.
|
|||
|
||||
```yaml
|
||||
telemetry:
|
||||
local: true # local plane (default on)
|
||||
allow_aggregate: true # hard gate; pin false to forbid the aggregate plane entirely
|
||||
local: true # local telemetry (default on)
|
||||
allow_aggregate: true # hard gate; pin false to forbid aggregate metrics entirely
|
||||
consent_state: unknown # aggregate opt-in: unknown | local | aggregate
|
||||
install_id: "" # stable anon id; "" mints one; clear to rotate
|
||||
retention_days: 90 # local event-log retention
|
||||
|
|
@ -189,7 +191,7 @@ layer (`/etc/hermes/config.yaml`), which wins over the user's value on a per-key
|
|||
There is no telemetry-specific policy block — to lock down a fleet, pin the keys you care
|
||||
about. Common examples:
|
||||
|
||||
- `telemetry.allow_aggregate: false` — the aggregate plane stays off even if
|
||||
- `telemetry.allow_aggregate: false` — aggregate metrics stay off even if
|
||||
`consent_state` is set to `aggregate`.
|
||||
- `telemetry.export.otlp.endpoint` — point every install at the corporate collector.
|
||||
- `telemetry.trajectories.enabled` — centrally decide whether content export is allowed.
|
||||
|
|
@ -202,8 +204,8 @@ secret values.
|
|||
## Privacy summary
|
||||
|
||||
- Local telemetry never leaves your machine.
|
||||
- The aggregate plane (the only thing that could go to Nous) is opt-in, default-off,
|
||||
and has no uploader today — nothing is sent.
|
||||
- Aggregate metrics (the only thing that could go to Nous) are opt-in, default-off,
|
||||
and have no uploader today — nothing is sent.
|
||||
- All export surfaces (file, OTLP) point at *your* destinations.
|
||||
- Secrets are always redacted on export; content export is off until you enable the
|
||||
trajectories plane; PII redaction is a knob.
|
||||
- Secrets are always redacted on export; content export is off until you enable
|
||||
trajectories; PII redaction is a knob.
|
||||
|
|
|
|||
|
|
@ -2175,7 +2175,7 @@ DEFAULT_CONFIG = {
|
|||
"redact_pii": False, # When True, hash user IDs and strip phone numbers from LLM context
|
||||
},
|
||||
|
||||
# Telemetry & observability. Three planes with a hard wall between them:
|
||||
# Telemetry & observability. Three settings, isolated from each other:
|
||||
# local — full-fidelity local observability the user owns (real
|
||||
# models, providers, tool names). Default ON.
|
||||
# aggregate — opt-in metadata to Nous (no uploader ships yet). Default OFF.
|
||||
|
|
@ -2186,30 +2186,30 @@ DEFAULT_CONFIG = {
|
|||
# value on a per-key basis. There is no telemetry-specific policy block — pin
|
||||
# e.g. `telemetry.allow_aggregate: false` in managed scope to hard-forbid egress.
|
||||
"telemetry": {
|
||||
# Local plane: event log + SQLite index in state.db. The user's own data;
|
||||
# never leaves the machine unless they export it or opt into aggregate.
|
||||
# Local telemetry: event log + SQLite index in state.db. The user's own data;
|
||||
# never leaves the machine unless they export it or opt into aggregate metrics.
|
||||
"local": True,
|
||||
# Hard gate for the aggregate plane. When False, the aggregate plane is off
|
||||
# Hard gate for aggregate metrics. When False, aggregate metrics are off
|
||||
# regardless of consent_state. Intended to be pinned False by an administrator
|
||||
# via managed scope on locked-down or air-gapped deployments. Default True
|
||||
# (consent_state still governs the opt-in).
|
||||
"allow_aggregate": True,
|
||||
# Aggregate-plane consent — the single source of truth for the opt-in:
|
||||
# Aggregate-metrics consent — the single source of truth for the opt-in:
|
||||
# "unknown" (no choice made — never uploads), "local" (declined),
|
||||
# "aggregate" (opted in). Set with `hermes config set telemetry.consent_state`
|
||||
# or a managed-scope pin. Non-interactive installs sit at "unknown".
|
||||
"consent_state": "unknown",
|
||||
# Stable install identifier (aggregate plane only). Empty string means "mint a
|
||||
# fresh UUID on first use"; clear it to rotate. Never sent on the local plane.
|
||||
# Stable install identifier (aggregate metrics only). Empty string means "mint a
|
||||
# fresh UUID on first use"; clear it to rotate. Never sent by local telemetry.
|
||||
"install_id": "",
|
||||
# Local event-log retention before rotation (days). Local plane only.
|
||||
# Local event-log retention before rotation (days). Local telemetry only.
|
||||
"retention_days": 90,
|
||||
# Keep secret redaction on even at full local capture (a SIEM full of live
|
||||
# credentials is a bigger attack target). Admin may override via managed scope.
|
||||
"redact_secrets": True,
|
||||
# Content redaction for exports / support bundles: "none" | "pii".
|
||||
"content_redaction": "none",
|
||||
# Trajectories plane: full message content / reasoning / raw tool args.
|
||||
# Trajectories: full message content / reasoning / raw tool args.
|
||||
# Off by default. When enabled, full content becomes exportable to the
|
||||
# configured destination — always secret-redacted, and PII-redacted per
|
||||
# content_redaction. This is the consent gate for content export and is
|
||||
|
|
|
|||
|
|
@ -14339,17 +14339,17 @@ def cmd_telemetry(args):
|
|||
if action == "status":
|
||||
print("Telemetry status")
|
||||
print("─" * 56)
|
||||
print(f" Local plane: {'on' if local_enabled else 'off'} "
|
||||
print(f" Local telemetry: {'on' if local_enabled else 'off'} "
|
||||
f"(telemetry.local)")
|
||||
print(f" Aggregate plane: {'on' if aggregate_enabled else 'off'} "
|
||||
print(f" Aggregate metrics: {'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")
|
||||
print(" opt in: hermes config set telemetry.consent_state aggregate")
|
||||
if not allow_aggregate:
|
||||
print(" ⚠ allow_aggregate is false (egress hard-disabled)")
|
||||
print(f" Install id: {install_id}")
|
||||
print(" Upload: DISABLED — no server yet. Aggregate is computed "
|
||||
"locally only.")
|
||||
print(" ⚠ allow_aggregate is false (egress hard-disabled)")
|
||||
print(f" Install id: {install_id}")
|
||||
print(" Upload: DISABLED — no server yet. Aggregate metrics are "
|
||||
"computed locally only.")
|
||||
print()
|
||||
# Export posture — where YOUR data can flow (never Nous). Values only;
|
||||
# lock-state is managed scope's concern, surfaced by its own tooling.
|
||||
|
|
@ -14371,12 +14371,12 @@ def cmd_telemetry(args):
|
|||
print(f" SDK: {_sdk}")
|
||||
else:
|
||||
print(" OTLP: disabled (telemetry.export.otlp.enabled)")
|
||||
# Content gate (trajectories plane) + redaction posture.
|
||||
# Content gate (telemetry.trajectories) + redaction posture.
|
||||
if redaction.content_export_enabled(config):
|
||||
print(f" Content export: on (trajectories plane) — message "
|
||||
print(f" Content export: on (trajectories enabled) — message "
|
||||
f"content exportable")
|
||||
else:
|
||||
print(" Content export: off (trajectories plane) — structural "
|
||||
print(" Content export: off (trajectories disabled) — structural "
|
||||
"telemetry only")
|
||||
print(" Secret redaction: on (always)")
|
||||
print(f" PII redaction: {redaction.content_mode_for(config)}")
|
||||
|
|
@ -14468,7 +14468,7 @@ def cmd_telemetry(args):
|
|||
content_ok = redaction.content_export_enabled(config)
|
||||
if want_content and not content_ok:
|
||||
print("⚠ --include-content ignored: telemetry.trajectories.enabled is false.")
|
||||
print(" Enable the trajectories plane (admin/config) to export message content.")
|
||||
print(" Enable trajectories (admin/config) to export message content.")
|
||||
print(" Exporting structural telemetry only.")
|
||||
|
||||
if not getattr(args, "out", None):
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"""``hermes telemetry`` subcommand parser.
|
||||
|
||||
Telemetry control and inspection. ``preview`` shows the per-run summary events that
|
||||
would be produced for the aggregate plane; there is no uploader, so it terminates as a
|
||||
would be produced for aggregate metrics; there is no uploader, so it terminates as a
|
||||
local view.
|
||||
|
||||
The handler is injected to avoid importing ``main`` (mirrors the insights subcommand).
|
||||
|
|
@ -18,14 +18,14 @@ def build_telemetry_parser(subparsers, *, cmd_telemetry: Callable) -> None:
|
|||
"telemetry",
|
||||
help="Inspect local telemetry and export it",
|
||||
description=(
|
||||
"Local-first telemetry. The local plane records observability on this "
|
||||
"machine. The aggregate plane is opt-in (set telemetry.consent_state via "
|
||||
"`hermes config set`); it has no uploader and is shown only via `preview`."
|
||||
"Local-first telemetry. Local telemetry records observability on this "
|
||||
"machine. Aggregate metrics are opt-in (set telemetry.consent_state via "
|
||||
"`hermes config set`); they have no uploader and are shown only via `preview`."
|
||||
),
|
||||
)
|
||||
sub = p.add_subparsers(dest="telemetry_action")
|
||||
|
||||
sub.add_parser("status", help="Show telemetry planes, consent state, and local data volume")
|
||||
sub.add_parser("status", help="Show telemetry settings, consent state, and local data volume")
|
||||
|
||||
prev = sub.add_parser(
|
||||
"preview",
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
"""Telemetry plugin — wires Hermes lifecycle hooks to the local-plane emitter.
|
||||
"""Telemetry plugin — wires Hermes lifecycle hooks to the local telemetry emitter.
|
||||
|
||||
This is the *only* instrumentation seam. It registers observational hooks (which core
|
||||
already invokes fail-open) and translates each into a typed local-plane telemetry
|
||||
event handed to ``agent.telemetry.emitter``. There are zero edits to core call sites:
|
||||
already invokes fail-open) and translates each into a typed local telemetry event
|
||||
handed to ``agent.telemetry.emitter``. There are zero edits to core call sites:
|
||||
the hooks already carry model/provider/usage/duration/tool data.
|
||||
|
||||
Everything here is best-effort and fail-open — a raised exception in a hook callback is
|
||||
swallowed by core, and we additionally guard each callback so a telemetry bug can never
|
||||
disturb a session. No content, no network: local plane only.
|
||||
disturb a session. No content, no network: local telemetry only.
|
||||
|
||||
Hooks consumed:
|
||||
on_session_start -> begin a run context (trace_id/run_id), buffer a run row
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ def test_preview_shows_real_values(home, capsys):
|
|||
|
||||
def test_status_reflects_consent_set_via_config(home, capsys):
|
||||
# Opting in is a plain config write now (no `enable` verb). status should
|
||||
# reflect consent_state=aggregate as the aggregate plane being on.
|
||||
# reflect consent_state=aggregate as aggregate metrics being on.
|
||||
from hermes_cli.config import load_config, save_config
|
||||
cfg = load_config()
|
||||
cfg.setdefault("telemetry", {})["consent_state"] = "aggregate"
|
||||
|
|
@ -70,17 +70,17 @@ def test_status_reflects_consent_set_via_config(home, capsys):
|
|||
_run("status")
|
||||
out = capsys.readouterr().out
|
||||
assert "consent_state=aggregate" in out
|
||||
assert "Aggregate plane: on" in out
|
||||
assert "Aggregate metrics: on" in out
|
||||
|
||||
|
||||
def test_status_shows_optin_hint_when_unknown(home, capsys):
|
||||
_run("status")
|
||||
out = capsys.readouterr().out
|
||||
assert "Aggregate plane: off" in out
|
||||
assert "Aggregate metrics: off" in out
|
||||
assert "config set telemetry.consent_state aggregate" in out
|
||||
|
||||
|
||||
def test_allow_aggregate_false_keeps_plane_off_in_status(home, capsys):
|
||||
def test_allow_aggregate_false_keeps_metrics_off_in_status(home, capsys):
|
||||
# Even with consent opted in, a managed allow_aggregate:false wins.
|
||||
from hermes_cli.config import load_config, save_config
|
||||
cfg = load_config()
|
||||
|
|
@ -90,5 +90,5 @@ def test_allow_aggregate_false_keeps_plane_off_in_status(home, capsys):
|
|||
save_config(cfg)
|
||||
_run("status")
|
||||
out = capsys.readouterr().out
|
||||
assert "Aggregate plane: off" in out
|
||||
assert "Aggregate metrics: off" in out
|
||||
assert "allow_aggregate is false" in out
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
Invariants:
|
||||
* Secrets ALWAYS stripped, every mode, no flag disables it.
|
||||
* Content gated by the trajectories plane, not a redaction mode.
|
||||
* Content gated by telemetry.trajectories, not a redaction mode.
|
||||
* PII stripped in 'pii' mode; structure preserved (codec-aware).
|
||||
"""
|
||||
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ def test_export_block_reflects_trajectories_gate(home, capsys):
|
|||
c.setdefault("telemetry", {})["trajectories"] = {"enabled": True}
|
||||
save_config(c)
|
||||
out = _status(capsys)
|
||||
assert "Content export: on (trajectories plane)" in out
|
||||
assert "Content export: on (trajectories enabled)" in out
|
||||
|
||||
|
||||
def test_token_env_not_set_shows_not_set(home, capsys):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue