Skip to content

feat: the agent names itself in the request it makes (ADR 0083) - #124

Merged
kfeofantov merged 1 commit into
mainfrom
feat/agent-names-itself-in-the-request
Sep 16, 2026
Merged

kfeofantov merged 1 commit into
mainfrom
feat/agent-names-itself-in-the-request

Conversation

@kfeofantov

Copy link
Copy Markdown
Contributor

Every request now carries User-Agent: runtime-agent/<version>.

Why the request rather than a payload

The agent stated its version in exactly one place: inside collection_coverage, as agent.version — a field about the sender riding in a document about the cluster. Three things follow from that, and none is about convenience:

  • The answer arrives on that payload's cadence. A delivery of any other kind says nothing about what sent it, and between two coverage flushes the question has no answer at all.
  • Only a reader that already parses payloads can get it. An access log, a proxy, a packet capture — everything that sees requests rather than documents — sees a sender that will not say what it is.
  • A fact about the sender is coupled to the schema of a fact about the cluster. They change for unrelated reasons.

A version is not a fact about the cluster. It describes what software made the request, which is the question User-Agent has always answered.

A standard header rather than an X- one: an X- name is likelier to be dropped by an intermediary, and it is one more name for an operator to learn before they can read their own traffic.

A version that cannot be stated is not stated

The build stamps the version from git describe, so its content comes from a tag, and a tag is a string this repository does not control. What may be stated is bounded here — at most 64 characters of what a semantic version is made of, plus the letters that spell the dev an unstamped build carries.

Outside that the product travels alone, runtime-agent, naming the agent and claiming nothing about which build it is:

Version stamped Sent
1.4.2 runtime-agent/1.4.2
dev runtime-agent/dev
v1.4.2-3-gb3691ed-dirty runtime-agent/v1.4.2-3-gb3691ed-dirty
1.4.2+build.5 runtime-agent/1.4.2+build.5
release/1.4 runtime-agent
1.4.2\r\nX-Scope: acme runtime-agent

Sending the value anyway is worse than it looks. A tag holding a space or a / produces something that is not a version in the shape the header agreed to; a tag holding a CRLF does not produce a bad version but a second header. Bounding it at the source means no reader has to be the one that notices.

Half an answer is the right answer here, because a missing version is a case any reader must already handle: an agent built before this header existed sends none, and an intermediary may strip one that was sent.

Nothing new leaves the cluster

collection_coverage.agent.version already carried this exact string, from the same variable. What changes is the channel, not the data — which is why security.md gains a pointer rather than a promise.

The payload keeps its field, and deliberately. The local sink writes byte-identical payloads to what the backend sink transmits (CLAUDE.md invariant 2), and a header is not part of a payload. Moving the version out of the body would make it invisible to everyone reading a local sink — the configuration where no request is made at all.

Verification

The wire is asserted, not the computed value: a test server records the User-Agent of every request a shipping round makes, and every one must carry it. The table above is a second test over userAgent directly, including the CRLF case.

go test ./...                    pass
golangci-lint (pinned v2.11.4)   0 issues
git archive HEAD -> clean tree   builds and tests green

🤖 Generated with Claude Code

https://claude.ai/code/session_01KeZECJXTDHL6Nts95eKMJA

Every request now carries `User-Agent: runtime-agent/<version>`.

The agent stated its version in one place: inside `collection_coverage`, as
a field about the sender riding in a document about the cluster. So the
answer arrived on that payload's cadence — between two coverage flushes the
question had none — and only a reader that already parses payloads could
get it. An access log, a proxy, a packet capture saw a sender that would
not say what it was.

A version is not a fact about the cluster. It describes what software made
the request, which is what User-Agent has always answered.

A version that cannot be stated is not stated. The build stamps it from
`git describe`, so its content comes from a tag, and a tag is a string this
repository does not control: bounded here at 64 characters of what a
semantic version is made of. Outside that the product travels alone,
naming the agent and claiming nothing about which build it is — a tag
holding a space or a slash is not a version in the shape the header agreed
to, and one holding a CRLF is not a version at all but a second header.

Nothing new leaves the cluster: `collection_coverage.agent.version` already
carried this exact string, from the same variable. The field stays, and
deliberately — the local sink writes byte-identical payloads to what the
backend sink transmits (invariant 2), and a header is not part of a
payload, so moving the version out of the body would hide it from everyone
reading a local sink.

    go test ./...                    pass
    golangci-lint (pinned v2.11.4)   0 issues
    the wire is asserted, not the value: a test server records the header
    of every request a round makes

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeZECJXTDHL6Nts95eKMJA
@kfeofantov
kfeofantov merged commit a2f4144 into main Sep 16, 2026
4 checks passed
@kfeofantov
kfeofantov deleted the feat/agent-names-itself-in-the-request branch September 16, 2026 12:26
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.

1 participant