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:
emozilla 2026-06-25 23:39:17 -04:00 committed by Victor Kyriazakos
parent e829d03fec
commit 72b7af73bd
17 changed files with 105 additions and 103 deletions

View file

@ -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)

View file

@ -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

View file

@ -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:

View file

@ -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)}

View file

@ -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"]

View file

@ -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 = {

View file

@ -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``.

View file

@ -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).

View file

@ -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).

View file

@ -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.

View file

@ -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

View file

@ -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):

View file

@ -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",

View file

@ -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

View file

@ -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

View file

@ -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).
"""

View file

@ -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):