Skip to main content

Observe the guardrail

Goal: see whether the guardrail is alive and actually enforcing, follow one blocked call to the decision that refused it, and get both into the observability stack you already run.

The SDK serves no dashboard of its own. It emits primitives and ships importable artifacts; you own the backend.

1. Turn telemetry on​

Four environment variables, no code change:

pip install -e 'backend[otel]'
export AGENTIC_AUTH_TELEMETRY_ENABLED=true
export AGENTIC_AUTH_TELEMETRY_KEY="$(openssl rand -hex 32)"
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318

AGENTIC_AUTH_TELEMETRY_KEY is required. It keys the identity pseudonyms in traces and logs and the session-derived trace ids. Leave it empty and the exporter refuses to start and logs once — the process keeps serving, because it will not export identities in the clear or behind an unkeyed digest that anyone holding the telemetry could reverse.

Each enforcement point configures its own, under AGENTIC_AUTH_SIDECAR_TELEMETRY_*. There is deliberately no fleet-wide switch.

With nothing configured at all, the local surfaces still work: GET /metrics, GET /__agentic/metrics, and JSON logs on stdout.

2. Scrape both surfaces​

scrape_configs:
- job_name: agentic-auth-control-plane
static_configs:
- targets: ['control-plane:8000']

- job_name: agentic-auth-pep
metrics_path: /__agentic/metrics
static_configs:
- targets: ['127.0.0.1:8081']

:::warning An enforcement point that is not scraped is not observable AgenticAuthPepSilent fires on absent(agentic_auth_pep_up). A sidecar bound to loopback and never scraped produces no series at all, so absence cannot be told from "never deployed". This is the one requirement the SDK meets by contract rather than in code. :::

3. Import the dashboards and alerts​

# Grafana → Dashboards → Import → upload each file
observability/dashboards/guardrail-health.json
observability/dashboards/rollout-tuning.json
observability/dashboards/chain-integrity.json
# prometheus.yml
rule_files:
- observability/alerts/guardrail.rules.yml

4. Check it is enforcing​

curl -s localhost:8081/__agentic/status | jq
{
"mode": "enforcing",
"uptime_seconds": 10432,
"upstreams": 3,
"upstream_map_digest": "sha256:9f2c...",
"pdp": { "reachable": true, "last_success_seconds_ago": 0.4 },
"telemetry": { "enabled": true, "state": "enabled", "identity_disclosure": false },
"version": "0.1.0"
}

mode is the answer to "is this thing actually enforcing". mode: "observing" decides and records but does not block — intended during a rollout, and a problem if nobody remembers.

pdp.reachable comes from the last real control-plane call, not a probe, so it cannot report healthy while real calls are failing.

5. Tell a working guardrail from a broken one​

This is the distinction the layer exists for. From the agent's side, both are just a refused call:

sum by (cause) (rate(agentic_auth_pep_blocks_total[5m]))
causeMeaning
policyPolicy said no. The guardrail worked.
pdp_unreachable, exchange_unreachable, mint_unreachable, audit_unavailableThe guardrail is fail-closing because it is broken. These page.
unauthorized, unresolvable_call, body_too_large, unbindableThe caller's or the operator's mistake. These do not page.

6. Follow one blocked call to its chain​

With a caller that sends traceparent, every hop lands in one trace. Open the denying authz.decide span: it carries the outcome, the deny reason, the hop, the policy version, and authz.action_id.

Take that action id to the audit API, which is where identities live:

curl -H "Authorization: Bearer $AGENTIC_AUTH_AUDIT_READ_TOKEN" \
"$CONTROL_PLANE/v1/audit/actions/$ACTION_ID/chain"

That returns the verified chain back to the consenting human.

:::info A trace id is never evidence A caller can claim any traceparent, so a trace id is a correlation handle only. The audit record, keyed by action_id, is the evidence — which is why every span carries one. :::

If the caller sends nothing, the decisions still form a single trace: the control plane and the enforcement point independently derive the same root from the session id, with nothing passed between them.

7. Before you enforce: see what would break​

Run the enforcement points in observe mode with taint enforcement off, then watch rollout-tuning until the would-be blocks reach zero:

sum(rate(agentic_auth_pep_calls_total{outcome="observed_would_block"}[5m]))
sum by (reason) (increase(agentic_auth_denials_total[1h]))
sum by (propagation) (increase(agentic_auth_taint_marks_total[1h]))

Metric labels carry no identities, so "which reason" hands off to GET /v1/audit/decisions?outcome=deny&deny_reason=<reason> for "which calls".

What never appears in telemetry​

No token, credential, request or response body, params_hash, binding, query string or stack trace — at any setting. Identities are pseudonymous unless you set AGENTIC_AUTH_TELEMETRY_IDENTITY_DISCLOSURE=true, and agentic_auth_telemetry_identity_disclosure reports which posture a deployment is in so you can alert on it.

See the telemetry surface reference for the full enumerated list.

If you run an OTLP-only pipeline​

Metrics stay on Prometheus exposition so the published metric names keep working. Collect them with the Collector's prometheus receiver — the snippet is in observability/README.md, and docker compose --profile observability up -d runs a working local version of exactly that.