User attribution
Every agent action can be traced back to a requester. The full design rationale is in docs/designs/audit-logging-user-attribution.md; this page is the operator-facing summary.
The attribution contract
Section titled “The attribution contract”| Plane | Carrier | Status |
|---|---|---|
| Agent traces | hermes.sender.id=<pseudonym>, user.id=<platform>:<pseudonym>, session.id=<hermes session> span attrs |
Implemented |
| Kubernetes actions | API audit entry naming the agent ServiceAccount | Implemented |
| LLM calls | OpenAI user field + metadata.requested_by on each request to LiteLLM |
Runtime follow-up |
| Created cluster objects | kubeagents.x-k8s.io/requested-by: <identity> annotation (+ optional request-id trace annotation) |
Runtime follow-up |
What ships today
Section titled “What ships today”- OTel defaults on the Platform Agent Deployment. Service name, namespace, and agent identity always; collector endpoint and OTLP protocol whenever the agent has somewhere to export. The endpoint is resolved per reconcile, and on a cluster where discovery finds no collector the operator omits it and sets
OTEL_SDK_DISABLED=trueinstead. Deploy → Telemetry owns the full ladder. - Session-to-user span enrichment. The
session_storeplugin persists requester metadata (platform and a pseudonymised user id) keyed by session, and thesession_otel_bridgeplugin reads it to stampsession.id,user.id, andhermes.sender.idonto spans. Both plugins run on the Planning Agent (default) profile, which owns chat ingress; work the Planning Agent delegates to the Platform Agent runs as a kanban worker whose card links back to the originating chat session. See Google Chat session metadata data flow. - Structured chat and tool audit records on stdout. Collected by GKE’s log agent without giving the workload direct write access to Cloud Logging.
- Reference API-server audit policy for self-managed clusters:
k8s-operator/config/audit/audit-policy.yaml.
Enable Managed OpenTelemetry on an existing cluster
Section titled “Enable Managed OpenTelemetry on an existing cluster”The installer enables Managed OTel on the clusters it installs to. For a cluster it did not touch:
gcloud container clusters update "$CLUSTER_NAME" \ --project "$PROJECT_ID" \ --location "$LOCATION" \ --managed-otel-scope=COLLECTION_AND_INSTRUMENTATION_COMPONENTSCheck version and release-channel prerequisites in the Managed OpenTelemetry setup guide. The in-cluster collector endpoint is http://opentelemetry-collector.gke-managed-otel.svc.cluster.local:4318.
GKE writes Kubernetes API audit entries to Cloud Logging automatically. Admin Activity audit logs are always on; Data Access audit logs are opt-in — see GKE audit logging.
Attribution annotations
Section titled “Attribution annotations”Runtime follow-up will set these on objects the agent creates:
kubeagents.x-k8s.io/requested-by— authenticated requester identity (Google Chat sender email today).kubeagents.x-k8s.io/request-id— OpenTelemetry trace ID, when available.
Annotations (not labels) because label values reject characters common in email addresses, and annotations avoid placing PII in selector indexes.
These are per-requester attribution on objects the agent creates, and are distinct from the project-identity labels the operator, kustomizations, and Helm chart stamp on the infrastructure kube-agents installs — see Resource labels.
Trust boundary
Section titled “Trust boundary”- The agent actor is recorded server-side by the Kubernetes API server as its ServiceAccount. This doesn’t depend on any workload-supplied annotation.
- The requester is asserted by the Chat adapter and session plugins from authenticated ingress metadata. Planned LLM fields and object annotations must be set by the trusted runtime, not generated by the model.
- Object annotations are supporting correlation data, not durable proof. They can be changed, are gone after deletion, and don’t identify the requester for unannotated updates.
- Server-generated logs are tamper-resistant only when retention and IAM prevent the agent from modifying the sink. Default provisioning grants the agent read-only log access; stronger environments should route an immutable copy to a separate security project.
- Prompts, model outputs, chat messages, and tool arguments can contain secrets or personal data. Restrict access and retention; redact sensitive values before export.
- The requester identity in spans, session rows, and audit records is a pseudonym, not an address. Google Chat reports the sender’s e-mail as their user id, so it is HMAC-SHA256 hashed with
SESSION_KV_SALTbefore anything is written; a Slack member id is already opaque and stays readable. The hash is stable while the salt is, so a person’s sessions still correlate — but resolving one back to a person means joining against the chat platform, which is the point. Rotating the salt breaks that correlation for every past session.
Query recipes
Section titled “Query recipes”Traces for a requester
Section titled “Traces for a requester”In Cloud Trace Explorer:
hermes.sender.id: "9f2c...b41e"The value is the salted HMAC of the requester’s chat identity, not their
address. To find someone’s traces, compute the digest the way
agents/chat/defaults/plugins/common/redactor.py does — HMAC-SHA256(salt, identity), hex-encoded, with the salt read from the SESSION_KV_SALT key of
the agent’s Secret.
Narrow to a specific session:
session.id: "20260702_153830_50074bf0"Agent Kubernetes mutations on GKE
Section titled “Agent Kubernetes mutations on GKE”In Cloud Logging:
resource.type="k8s_cluster"protoPayload.serviceName="k8s.io"protoPayload.authenticationInfo.principalEmail="system:serviceaccount:kubeagents-system:AGENT_SERVICE_ACCOUNT"The audit entry contains verb, resource, namespace, name, timestamp, and agent identity. On a self-managed cluster the equivalent field is user.username.
Objects annotated for a requester
Section titled “Objects annotated for a requester”After the cluster-object runtime follow-up lands:
kubectl get all,configmap,rolebinding -A -o json | \ jq --arg requester "alice@example.com" ' .items[] | select(.metadata.annotations["kubeagents.x-k8s.io/requested-by"] == $requester) | [.apiVersion, .kind, .metadata.namespace, .metadata.name] | @tsv 'LiteLLM calls for a requester
Section titled “LiteLLM calls for a requester”After the LLM runtime follow-up lands, filter LiteLLM records on metadata.requested_by="alice@example.com". Use the record’s trace ID to open the related Cloud Trace trace.
Join traces to chat logs
Section titled “Join traces to chat logs”Take session.id from a span and filter structured container logs:
jsonPayload.session_id="20260702_153830_50074bf0"Where to go next
Section titled “Where to go next”docs/designs/audit-logging-user-attribution.md— full design.- Concepts → Observability — telemetry export and Console link generation.