auth_events carries principal_kind and principal_id (added by the migration at crates/credentials-core/src/store.rs:199-200). The read surface never writes them.
crates/credentials-module/src/read_surface.rs:1181 constructs the observation with four fields:
let observation = credentials_core::store::AuthObservation {
kind: if refreshable { ... },
provider_status: Some(params.provider_status),
detail: None,
reporter_source: params.reporter_source.as_deref().map(ReporterSource::from_wire),
};
No principal field exists on the struct, so the columns are structurally NULL on consumer reports rather than merely unpopulated. On this deployment: 36 of 36 consumer_report_stale rows NULL, 133 of 133 rows NULL across every kind.
Why it matters is specific, not cosmetic. Identity is available on the audit row — actor holds conn-N — but audit rows are written only when the version gate passes (mark_stale_if_version_reported, store.rs:1656-1698: append_auth_event_tx runs unconditionally, append_audit_tx only if n > 0). So the reports that DO get attributed are the ones that changed state, and the reports that carry no identity are exactly the version-gated-out ones.
That is the wrong way round for diagnosis. Two distinct consumer behaviours produce an identical applied=0 row:
| behaviour |
row |
a consumer in flight with material the vault rotated past — reports v(n-1), honest |
applied=0, no audit row |
a consumer repeatedly re-serving a version it already reported — stale_pending already 1 |
applied=0, no audit row |
The first is benign and expects roughly one row per consumer per rotation. The second is a consumer-side defect and can produce many rows from one connection. Distinguishing them requires knowing how many distinct connections produced the cluster, and that is precisely what the gated-out rows do not record.
Concretely: an 8-row applied=0 cluster from 8 connections means eight caches went stale together and the fence did its job. The same 8 rows from one connection means a consumer is looping. Today those read identically.
Suggested shape: carry the connection actor onto AuthObservation and write it into the existing columns for report_auth_failure. The audit row already computes an actor at read_surface.rs:1175, so the value is in scope at the call site — this is a plumbing change, not new identity machinery.
Noting two constraints rather than assuming them away:
auth_events is trimmed to 64 rows per credential per insert (trim_auth_events_tx, store.rs:3710), so attribution is only ever available on the recent tail.
conn-N identifies a connection, not an OS process. Whether one consumer process holds one connection is a consumer property, so the column would support "how many distinct connections" and not, by itself, "how many processes".
auth_eventscarriesprincipal_kindandprincipal_id(added by the migration atcrates/credentials-core/src/store.rs:199-200). The read surface never writes them.crates/credentials-module/src/read_surface.rs:1181constructs the observation with four fields:No principal field exists on the struct, so the columns are structurally NULL on consumer reports rather than merely unpopulated. On this deployment: 36 of 36
consumer_report_stalerows NULL, 133 of 133 rows NULL across every kind.Why it matters is specific, not cosmetic. Identity is available on the audit row —
actorholdsconn-N— but audit rows are written only when the version gate passes (mark_stale_if_version_reported, store.rs:1656-1698:append_auth_event_txruns unconditionally,append_audit_txonlyif n > 0). So the reports that DO get attributed are the ones that changed state, and the reports that carry no identity are exactly the version-gated-out ones.That is the wrong way round for diagnosis. Two distinct consumer behaviours produce an identical
applied=0row:v(n-1), honestapplied=0, no audit rowstale_pendingalready 1applied=0, no audit rowThe first is benign and expects roughly one row per consumer per rotation. The second is a consumer-side defect and can produce many rows from one connection. Distinguishing them requires knowing how many distinct connections produced the cluster, and that is precisely what the gated-out rows do not record.
Concretely: an 8-row
applied=0cluster from 8 connections means eight caches went stale together and the fence did its job. The same 8 rows from one connection means a consumer is looping. Today those read identically.Suggested shape: carry the connection actor onto
AuthObservationand write it into the existing columns forreport_auth_failure. The audit row already computes anactoratread_surface.rs:1175, so the value is in scope at the call site — this is a plumbing change, not new identity machinery.Noting two constraints rather than assuming them away:
auth_eventsis trimmed to 64 rows per credential per insert (trim_auth_events_tx, store.rs:3710), so attribution is only ever available on the recent tail.conn-Nidentifies a connection, not an OS process. Whether one consumer process holds one connection is a consumer property, so the column would support "how many distinct connections" and not, by itself, "how many processes".