Ask
Add an optional detail parameter to credential.report_auth_failure, flowing into the AuthObservation.detail field that this path currently hardcodes to None (read_surface.rs, the AuthObservation built in report_auth_failure).
Why now
anthropic-auth#175 adds 401 reporting from a second observation path: the WebSocket relay, which parses the upstream status out of a relay_upstream_error SSE event (structured error.status first, regex over the message as fallback). That gives the vault two report origins with different provenance strength — a direct HTTP 401 seen by the client, and a relay-parsed one.
The version fence stops stale reports; it does not stop wrong ones. A relay parse false-positive at the still-current record version passes the fence, marks the credential stale, and forces a refresh. When that eventually happens, the forensic question is "which observation path produced this report" — and today's auth_events row cannot answer it: it records provider_status only, and the two paths both report 401.
The consumer is ready to thread "relay_upstream_error_parse" vs "direct_401" from its two call sites as soon as the field exists (confirmed with the anthropic-auth seat; no fence-semantics change wanted or expected).
Constraints from the schema's own rules
The migration-3 comment sets the discipline: detail carries a typed variant name and never provider body text, because adapter error values can echo submitted parameters and this table must never hold token material in a plaintext column.
A consumer-supplied value has to keep that property against a consumer that doesn't. Proposal:
- Wire:
detail: Option<String>, absent today = absent tomorrow — old consumers unaffected, no version bump semantics.
- Server-side validation, not trust: accept only
^[a-z0-9_]{1,64}$. Anything else is not an error — the report's state-machine effect must not depend on its diagnostic rider — the observation is written with detail = Some("invalid_detail_rejected"), which is itself a typed variant name recording that a consumer sent something out of contract.
- No effect on the version fence, the stale/latch arm choice, or rate limiting.
Out of scope
Reporter identity. The existing comment block in report_auth_failure already covers why conn-{id} is a route channel and not an identity, and what wiring Principal::Reserved would take. This issue is only about which observation path, which the consumer can state truthfully about itself and validation can bound.
Happy to carry the patch on the usual fork lane if the shape is agreed.
Ask
Add an optional
detailparameter tocredential.report_auth_failure, flowing into theAuthObservation.detailfield that this path currently hardcodes toNone(read_surface.rs, theAuthObservationbuilt inreport_auth_failure).Why now
anthropic-auth#175 adds 401 reporting from a second observation path: the WebSocket relay, which parses the upstream status out of a
relay_upstream_errorSSE event (structurederror.statusfirst, regex over the message as fallback). That gives the vault two report origins with different provenance strength — a direct HTTP 401 seen by the client, and a relay-parsed one.The version fence stops stale reports; it does not stop wrong ones. A relay parse false-positive at the still-current record version passes the fence, marks the credential stale, and forces a refresh. When that eventually happens, the forensic question is "which observation path produced this report" — and today's
auth_eventsrow cannot answer it: it recordsprovider_statusonly, and the two paths both report 401.The consumer is ready to thread
"relay_upstream_error_parse"vs"direct_401"from its two call sites as soon as the field exists (confirmed with the anthropic-auth seat; no fence-semantics change wanted or expected).Constraints from the schema's own rules
The migration-3 comment sets the discipline:
detailcarries a typed variant name and never provider body text, because adapter error values can echo submitted parameters and this table must never hold token material in a plaintext column.A consumer-supplied value has to keep that property against a consumer that doesn't. Proposal:
detail: Option<String>, absent today = absent tomorrow — old consumers unaffected, no version bump semantics.^[a-z0-9_]{1,64}$. Anything else is not an error — the report's state-machine effect must not depend on its diagnostic rider — the observation is written withdetail = Some("invalid_detail_rejected"), which is itself a typed variant name recording that a consumer sent something out of contract.Out of scope
Reporter identity. The existing comment block in
report_auth_failurealready covers whyconn-{id}is a route channel and not an identity, and what wiringPrincipal::Reservedwould take. This issue is only about which observation path, which the consumer can state truthfully about itself and validation can bound.Happy to carry the patch on the usual fork lane if the shape is agreed.