Skip to content

feat: send Auth0-CLI-Metadata header on Management API requests - #1616

Merged
ramya18101 merged 2 commits into
mainfrom
feat/invoker-metadata-header
Aug 18, 2026
Merged

feat: send Auth0-CLI-Metadata header on Management API requests#1616
ramya18101 merged 2 commits into
mainfrom
feat/invoker-metadata-header

Conversation

@developerkunal

Copy link
Copy Markdown
Contributor

🔧 Changes

Adds an Auth0-CLI-Metadata header to every Management API request so agent, CI, and human invocations can be distinguished. A dedicated header rather than extra User-Agent tokens 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-Client header so the server can reuse the same decode path.

Auth0-Cli-Metadata: eyJpbnZva2VyX2tpbmQiOiJhZ2VudCIsImludm9rZXJfYWdlbnQiOiJjbGF1ZGUtY29kZSIsImNpIjp0cnVlfQ==

{"invoker_kind":"agent","invoker_agent":"claude-code","ci":true}

New in internal/cli/invoker_metadata.go:

  • invokerMetadata struct with invoker_kind, invoker_agent, and ci
  • resolveInvokerMetadata(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 cached agentClientName() and isCIEnvironment() from root.go instead of adding new detection
  • invokerMetadataTransport, an http.RoundTripper that clones the request before setting the header

invoker_kind is a closed set of agent, ci, human. ci is reported separately so the combination survives: an agent running inside CI reports kind agent with ci: true, keeping both signals. ci carries no omitempty, because an explicit false means "not CI" while an absent field would mean "this CLI version did not report it".

detected agent CI invoker_kind invoker_agent
claude-code no agent claude-code
claude-code yes agent claude-code
unknown-agent either agent unknown
human (TTY) no human unknown
unknown (no TTY) no human unknown
human or unknown yes ci unknown

A missing agent signal resolves to human whether 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, customClientWithRetries takes the encoded value and wraps invokerMetadataTransport around rateLimitTransport and retryableErrorTransport, so the header is stamped once before any retry decision and preserved across retries. initializeManagementClient and initializeManagementClientV3 each take a new invokerMetadata string parameter, and cli.go computes the value once in setupWithAuthentication and 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 plus option.WithHTTPHeader() for v3, because v1's Header is a RequestOption that cannot be supplied at client construction. The transport also covers the passthrough auth0 api command.

The header constant is spelled Auth0-Cli-Metadata, Go's canonical form, because net/http canonicalizes header names so CLI would go out as Cli regardless. 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.go replaces the bare "unknown-agent" literal with an agentClientUnknownAgent constant. 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-mode and the agent detection this header reuses.

🔬 Testing

internal/cli/invoker_metadata_test.go covers three layers:

  • Resolution logic, table-driven across every agent and CI combination, asserting kind, agent name, and ci independently. Separate tests pin invoker_kind to its closed set across all detectAgent outputs, pin the absence of omitempty on ci, and assert the header constant equals http.CanonicalHeaderKey of itself.
  • Transport behaviour, that the header is set, that an empty payload omits it entirely rather than sending it blank, and that the original request is not mutated.
  • Real requests through the real SDK constructors. Three tests drive initializeManagementClient and initializeManagementClientV3 against an httptest.NewTLSServer over a genuine TLS handshake, with real certificate verification via an x509.CertPool holding the server's certificate. The server decodes the header and asserts the payload, and confirms User-Agent still arrives so the new header adds to existing telemetry rather than displacing it. A third test returns 503 then 200 and asserts the header is byte-identical on both attempts.

make lint and make test-unit pass. make docs produces no diff, since no command, flag, or help text changed.

📝 Checklist

  • All new/changed/fixed functionality is covered by tests (or N/A)
  • I have added documentation for all new/changed functionality (or N/A)

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.
@developerkunal
developerkunal requested a review from a team as a code owner August 17, 2026 13:42
@ramya18101
ramya18101 enabled auto-merge (squash) August 18, 2026 06:25
@ramya18101
ramya18101 merged commit 607503e into main Aug 18, 2026
6 checks passed
@ramya18101
ramya18101 deleted the feat/invoker-metadata-header branch August 18, 2026 06:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants