Documentation
¶
Overview ¶
Package clientsignals computes coarse, privacy-safe signals that help estimate whether a CLI process is being driven by a human or an AI agent.
This package is intentionally self-contained: no dependencies beyond the standard library and golang.org/x/sys (used for syscall-based parent-process lookup on Darwin/Windows), so it can be used standalone.
Index ¶
- Constants
- func TrackedRoute(method, routeTemplate, requestPath string, prefixes []string) (string, bool)
- type ClientSignalsTransport
- type RequestClassification
- type RequestCounter
- type Signals
- func (s Signals) ApplyHeaders(header http.Header)
- func (s Signals) ApplyHeadersWithPrefix(header http.Header, prefix string)
- func (s Signals) Operator() string
- func (s Signals) WrapTransport(inner http.RoundTripper) *ClientSignalsTransport
- func (s Signals) WrapTransportWithPrefix(inner http.RoundTripper, prefix string) *ClientSignalsTransport
Constants ¶
const ( // RequestOperatorCI identifies instrumented requests made from CI. RequestOperatorCI = "ci" // RequestOperatorAgent identifies instrumented, non-CI requests with a // valid cooperative agent marker. RequestOperatorAgent = "agent" // RequestOperatorInteractive identifies instrumented, non-CI requests // whose stdout was attached to a terminal and had no agent marker. RequestOperatorInteractive = "interactive" // RequestOperatorAutomatedUnattributed identifies instrumented, non-CI, // non-interactive requests with no recognized agent marker. RequestOperatorAutomatedUnattributed = "automated_unattributed" // RequestOperatorUninstrumented identifies requests without a valid // Fly-Client-Interactive header. RequestOperatorUninstrumented = "uninstrumented" // RequestAgentNone is the bounded metric value used when no valid agent // marker was supplied. RequestAgentNone = "none" // RequestAgentOther is the bounded metric value used for valid sanitized // self-declarations that are not in the known marker table. RequestAgentOther = "other" )
const DefaultHeaderPrefix = "Fly"
DefaultHeaderPrefix is the header-name prefix WrapTransport uses when no other prefix is configured: "Fly", giving Fly-Client-Interactive, Fly-Client-Parent, etc. Callers outside Fly.io can use WrapTransportWithPrefix to substitute their own.
const ( // RequestMetricName is the canonical Prometheus counter name for requests // classified using client signals. RequestMetricName = "fly_client_signals_requests_total" )
Variables ¶
This section is empty.
Functions ¶
func TrackedRoute ¶ added in v0.4.3
TrackedRoute returns the bounded route metric label for a request and whether it should be recorded. A matched route template is preferred. For an unmatched request, requestPath is used only to determine whether the request targeted a tracked prefix and is never included in the returned label.
Types ¶
type ClientSignalsTransport ¶
type ClientSignalsTransport struct {
InnerTransport http.RoundTripper
// contains filtered or unexported fields
}
ClientSignalsTransport wraps an http.RoundTripper, attaching the {prefix}-Client-* headers and appending the client-signals token to the existing User-Agent header on every outgoing request.
Construct one via Signals.WrapTransport or Signals.WrapTransportWithPrefix. RoundTrip does no detection work itself — it only applies the values already computed when the transport was built.
type RequestClassification ¶ added in v0.4.3
RequestClassification is the bounded, server-side interpretation of incoming client-signal headers. Operator and Agent are safe to use as metric labels.
func ClassifyRequestHeaders ¶ added in v0.4.3
func ClassifyRequestHeaders(header http.Header) RequestClassification
ClassifyRequestHeaders returns bounded metric-label values for an incoming request. Fly-Client-Interactive is the instrumentation sentinel; Parent is deliberately ignored because parent-process lookup is not reliable enough for request classification.
CI takes precedence over agent, which takes precedence over interactive. Agent is still preserved for CI requests so aggregate metrics can expose the CI+agent overlap.
type RequestCounter ¶ added in v0.4.3
type RequestCounter struct {
// contains filtered or unexported fields
}
RequestCounter is the canonical Prometheus collector for requests classified using client signals. It is safe for concurrent use.
func NewRequestCounter ¶ added in v0.4.3
func NewRequestCounter(service string, trackedRoutePrefixes []string) *RequestCounter
NewRequestCounter constructs the canonical client-signals request collector. The caller is responsible for registering it with a Prometheus registerer.
func (*RequestCounter) Collect ¶ added in v0.4.3
func (c *RequestCounter) Collect(ch chan<- prometheus.Metric)
Collect implements prometheus.Collector.
func (*RequestCounter) Describe ¶ added in v0.4.3
func (c *RequestCounter) Describe(ch chan<- *prometheus.Desc)
Describe implements prometheus.Collector.
func (*RequestCounter) Observe ¶ added in v0.4.3
func (c *RequestCounter) Observe(req *http.Request, routeTemplate string) bool
Observe records req when its matched route template or raw request path belongs to a configured prefix. routeTemplate should be the bounded template reported by the service's router, not the raw request path.
type Signals ¶
type Signals struct {
// Interactive is true if the process's stdout appears to be attached to
// a terminal.
Interactive bool
// Parent is a coarse bucket describing the immediate parent process.
// Always one of "node", "python", "shell", or "other" — never a raw
// process name.
Parent string
// Agent is the cooperative agent marker, e.g. "claude-code". Empty if no
// agent was declared or detected.
Agent string
// AgentSource identifies how Agent was determined, e.g.
// "env:FLY_INVOKED_BY" or "env:CLAUDECODE" — the matched variable name,
// never its value. Empty if and only if Agent is empty.
AgentSource string
// CI is true when a CI environment is detected.
CI bool
}
Signals is the set of coarse, privacy-safe traffic-classification signals computed once per process.
See docs/signals.md for the reasoning behind these fields, each one's known reliability caveats, and how they're meant to be combined — no single field here is sufficient on its own, and none should ever drive gating/enforcement decisions.
func Detect ¶
func Detect() Signals
Detect computes the current process's client signals fresh from the environment and file descriptors. It is pure and side-effect free (aside from reading process state); it does not cache its result — callers that want a single value for the lifetime of a process should cache it themselves.
func DetectOnce ¶
func DetectOnce() Signals
DetectOnce returns the process-wide signals, computed once via Detect and cached for the lifetime of the process. Detection involves a parent-process lookup and environment scanning, so callers should fetch this once (e.g. at client-construction time) and reuse the result rather than calling it per request.
func (Signals) ApplyHeaders ¶
ApplyHeaders sets the Fly-Client-* headers on header directly, for callers that need to attach client signals to something other than an http.Request going through an http.RoundTripper — e.g. a WebSocket handshake's header, built and sent outside of net/http's Client/Transport machinery entirely. Uses DefaultHeaderPrefix ("Fly").
func (Signals) ApplyHeadersWithPrefix ¶
ApplyHeadersWithPrefix is like ApplyHeaders but lets callers outside Fly.io substitute their own header prefix, matching WrapTransportWithPrefix's prefix for the same Signals value.
func (Signals) Operator ¶ added in v0.4.2
Operator returns a single classification for the process's operator. Precedence: ci > agent > interactive > unknown.
This is a convenience for consumers that want one label describing who is driving the process. The raw fields (CI, Agent, Interactive) remain available for callers that need finer-grained logic.
func (Signals) WrapTransport ¶
func (s Signals) WrapTransport(inner http.RoundTripper) *ClientSignalsTransport
WrapTransport wraps inner in a *ClientSignalsTransport that attaches s to every request the returned transport forwards, using DefaultHeaderPrefix ("Fly-Client-*").
func (Signals) WrapTransportWithPrefix ¶
func (s Signals) WrapTransportWithPrefix(inner http.RoundTripper, prefix string) *ClientSignalsTransport
WrapTransportWithPrefix is like WrapTransport but lets callers outside Fly.io substitute their own header prefix (e.g. "Acme" for Acme-Client-Interactive, Acme-Client-Parent, ...) instead of the "Fly" default. prefix must not be empty.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Command clientsignals prints the client signals that would be attached to outbound Fly API requests from the current process/environment.
|
Command clientsignals prints the client signals that would be attached to outbound Fly API requests from the current process/environment. |