feat: send Auth0-CLI-Metadata header on Management API requests - #1616
Merged
Conversation
Adds a base64-encoded JSON payload identifying the invoker (agent, ci or human) along with the agent name, so agent, CI and human traffic can be told apart in telemetry. The header is stamped by a transport wrapping the retry chain, so it is applied once and survives retries on both the v1 and v3 management clients.
ramya18101
approved these changes
Aug 18, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🔧 Changes
Adds an
Auth0-CLI-Metadataheader to every Management API request so agent, CI, and human invocations can be distinguished. A dedicated header rather than extraUser-Agenttokens gives us a place to add fields later without reparsing an existing value.The value is base64-encoded JSON, matching the encoding of the sibling
Auth0-Clientheader so the server can reuse the same decode path.New in
internal/cli/invoker_metadata.go:invokerMetadatastruct withinvoker_kind,invoker_agent, andciresolveInvokerMetadata(agentClient string, isCI bool)maps the existing detection result onto the payload(invokerMetadata).headerValue()renders base64-encoded JSON, returning empty on marshal failure so the header is omitted rather than sent blank(*cli).invokerMetadataHeaderValue()reuses the cachedagentClientName()andisCIEnvironment()fromroot.goinstead of adding new detectioninvokerMetadataTransport, anhttp.RoundTripperthat clones the request before setting the headerinvoker_kindis a closed set ofagent,ci,human.ciis reported separately so the combination survives: an agent running inside CI reports kindagentwithci: true, keeping both signals.cicarries noomitempty, because an explicitfalsemeans "not CI" while an absent field would mean "this CLI version did not report it".invoker_kindinvoker_agentclaude-codeagentclaude-codeclaude-codeagentclaude-codeunknown-agentagentunknownhuman(TTY)humanunknownunknown(no TTY)humanunknownhumanorunknownciunknownA missing agent signal resolves to
humanwhether or not a terminal is attached, since piping output or driving the CLI from a script is still a person.In
internal/cli/management.go,customClientWithRetriestakes the encoded value and wrapsinvokerMetadataTransportaroundrateLimitTransportandretryableErrorTransport, so the header is stamped once before any retry decision and preserved across retries.initializeManagementClientandinitializeManagementClientV3each take a newinvokerMetadata stringparameter, andcli.gocomputes the value once insetupWithAuthenticationand passes it to both. Both functions are unexported, so this is not a breaking change.One transport is used rather than
management.Header()for v1 plusoption.WithHTTPHeader()for v3, because v1'sHeaderis aRequestOptionthat cannot be supplied at client construction. The transport also covers the passthroughauth0 apicommand.The header constant is spelled
Auth0-Cli-Metadata, Go's canonical form, becausenet/httpcanonicalizes header names soCLIwould go out asCliregardless. Header names are case-insensitive per RFC 9110 §5.1 and HTTP/2 lower-cases them, so the server must match case-insensitively. A test pins this so the constant cannot drift from the wire.internal/cli/agent_detection.goreplaces the bare"unknown-agent"literal with anagentClientUnknownAgentconstant. It appeared in two places and is now referenced in a third, so a rename would previously have compiled cleanly while silently breaking the mapping.📚 References
Builds on #1612, which added
--agent-modeand the agent detection this header reuses.🔬 Testing
internal/cli/invoker_metadata_test.gocovers three layers:ciindependently. Separate tests pininvoker_kindto its closed set across alldetectAgentoutputs, pin the absence ofomitemptyonci, and assert the header constant equalshttp.CanonicalHeaderKeyof itself.initializeManagementClientandinitializeManagementClientV3against anhttptest.NewTLSServerover a genuine TLS handshake, with real certificate verification via anx509.CertPoolholding the server's certificate. The server decodes the header and asserts the payload, and confirmsUser-Agentstill arrives so the new header adds to existing telemetry rather than displacing it. A third test returns503then200and asserts the header is byte-identical on both attempts.make lintandmake test-unitpass.make docsproduces no diff, since no command, flag, or help text changed.📝 Checklist