Agent implementation & releases
Every call through Brutor carries three different facts about its caller, and the platform keeps them apart:
| Fact | Question | Source of truth | Trust |
|---|---|---|---|
| Identity | Who is acting? | The credential: an API key bound to an agent identity, or a verified workload token (OIDC/SPIFFE) | Verified |
| Implementation | What software is acting? | The agent’s declared name, or a claim inside its verified token | verified or declared |
| Release | Which version / build of it? | The agent’s declared version (and build digest), or a verified claim | verified or declared |
Identity is unchanged and stays the only thing that decides access. Implementation and release describe: they are recorded on every call and run, declared and approved per agent identity, and watched for change — but nothing an agent declares about itself can change its identity, its grants or any access decision.
What an agent should send
Section titled “What an agent should send”The gateway reads the first carrier present, in this order of trust:
| # | Carrier | Name | Version | Build | Trust |
|---|---|---|---|---|---|
| 1 | Verified workload token claims (RFC 7591 names) | software_id |
software_version |
software_build |
verified |
| 2 | MCP clientInfo — _meta["io.modelcontextprotocol/clientInfo"] on the request (MCP 2026-07-28), else the session’s initialize params.clientInfo |
name |
version |
— | declared |
| 3 | W3C baggage (OpenTelemetry GenAI names) |
gen_ai.agent.name |
gen_ai.agent.version |
gen_ai.agent.build |
declared |
| 4 | Brutor headers (for clients that speak none of the above) | X-Brutor-Agent-Name |
X-Brutor-Agent-Version |
X-Brutor-Agent-Build |
declared |
A declared value never overrides a verified claim. Values are normalised and bounded —
name [a-z0-9._-]{1,64} (lower-cased), version [A-Za-z0-9.+_-]{1,64}, build
[A-Za-z0-9:._-]{1,128} (a git SHA or an image digest such as sha256:…); anything else is
dropped, never stored raw. Caller baggage is not forwarded upstream by the gateway.
The HTTP User-Agent is never a name source.
A typical HTTP agent sends its package name and version, and the image digest when it has one:
headers = { "X-Brutor-Agent-Name": "loan-screening-agent", "X-Brutor-Agent-Version": importlib.metadata.version("loan-screening-agent"), "X-Brutor-Agent-Build": os.environ.get("BRUTOR_AGENT_BUILD", ""), # e.g. sha256:…}An MCP client sets clientInfo on initialize (every MCP SDK does) or in the per-request
_meta. An agent that already exports OpenTelemetry sets the gen_ai.agent.* baggage
members.
Client labels
Section titled “Client labels”A client is the channel a request came through — the portal, an SDK integration, a named
app — never the HTTP library. The run’s client_label is X-Brutor-Client when present,
else the product name of the first User-Agent token without its version
(python-httpx/0.28.1 → python-httpx; a browser’s Mozilla/5.0 (…) → browser).
A system’s own member agent is never judged as its client: see
Conformance.
What is recorded
Section titled “What is recorded”- On every call (
proxy_logs): the resolved name, version, build and carrier (token·mcp_client_info·baggage·header), plus the normalised client label. The rawUser-Agentis kept as it was. - On every run: the release of its first action that carries one, its trust, and an
implementation fingerprint — a hash over the first action’s full
User-Agent, the MCP SDK’s ownclientInfoand the instruction fingerprint. It answers “does this look like the same software?” without storing any of the inputs. - On every segment a shared service serves: the delegate’s release, so a service’s releases are visible to its callers.
- One release row per
(agent identity, name, version, build)observed, with first and last seen, run count, trust and the distinct fingerprints seen for it.
Declaring and approving releases
Section titled “Declaring and approving releases”Each agent identity carries an Implementation declaration (Admin Console: Agents → Identities → identity → Implementation):
| Field | Meaning |
|---|---|
implementation_name |
The name this identity’s software must declare. Empty = not declared. |
approved_versions |
The versions an approver accepts — exact (1.4.2) or a prefix ending in .* (1.4.* accepts 1.4.2 and 1.4.2.1, never 1.40 or 1.4). Empty = observe only. |
require_release |
A run that carries no name or version is a finding. |
All three are part of the contract of every AI System the identity
is a member of. Changing them drifts the contract and needs re-approval like any other term;
the contract diff reads approving a version as widened and withdrawing one as
narrowed. Whether a release is approved is never stored — it is derived from
approved_versions whenever it is read, so approving a version applies to every release at
once, from that moment on (runs made before it are judged by what was approved then — see
Findings).
The identity’s release timeline lists each release with first/last seen, run count, the
trust badge, whether it is approved and how many implementation fingerprints it has shown.
On an AI System, the Composition tab shows each member agent as name@version with its
trust badge, and the Lifecycle tab records an agent.new_release event the first time a
member agent runs a new release.
Findings
Section titled “Findings”The agent checks run with the platform’s assurance checks and raise into the Assurance Inbox under Agent:
| Code | Fires when | Severity | Clears when |
|---|---|---|---|
agent.unapproved_release |
a run in the last 7 days whose version matched none of the approved_versions in effect when it started |
high on a high-risk system, else medium | no such run remains in the last 7 days — approving the version stops new ones |
agent.name_mismatch |
the current release’s name differs from implementation_name |
high | the declared name matches again |
agent.release_missing |
require_release is set and the latest run carries no name or version |
medium | runs carry it again |
agent.undeclared_change |
the same (name, version, build) shows a new implementation fingerprint after at least 20 runs on others |
high | acknowledged, or a new version is declared |
A release is judged by when it ran. Every change to approved_versions is recorded with
the moment it took effect, and each run is judged by the declaration in effect when it
started. Replacing 0.1.0 with 0.2.0 therefore does not make 0.1.0 — approved the whole
time it ran — a finding, while runs of 0.2.0 that started before it was approved are one
(the finding names the first such run and how many there were). A run that starts at the
very moment a declaration changes is judged by the new one. An empty declaration approves
nothing and constrains nothing. To accept an unapproved run you have reviewed, close its
drift finding as accepted_change; otherwise it clears once the run is more than 7 days old.
The last one is the most valuable: a release that keeps its version but behaves like different software is the change nobody announced.
Each finding is also an open drift finding of class agent on every AI System the
identity is a member of, so a response policy can act on it — for
example, an unapproved release on a high-risk system moves the system to
approval_required:
on: drift_class: agent severity_at_least: highthen: - set_autonomy: approval_requiredThese findings do not count as the system’s own behavioural drift. Drift itself gains the
cause agent_release: a release transition within the change-point window explains the
drift and names both releases. The assurance report
lists each member agent’s current release, its trust and its approval, and the agent
findings, in its composition section.
| Method | Path | Purpose |
|---|---|---|
| POST / PATCH | /v1/admin/agent-identities[/{agent_id}] |
Set implementation_name, approved_versions, require_release |
| GET | /v1/admin/agent-identities/{agent_id}/releases |
The release timeline (approved derived; current_release_id) |
| GET | /v1/admin/ai-systems/{id}/composition |
agents[].release — each member agent’s current release |
| GET | /v1/admin/ai-systems/{id}/lifecycle |
events[] — agent.new_release entries |
Declare and approve (Control Plane, :5050):
curl -X PATCH http://localhost:5050/v1/admin/agent-identities/agent-01J9Z3 \ -H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \ -d '{"implementation_name": "loan-screening-agent", "approved_versions": ["1.4.*", "1.3.2"], "require_release": true}'{ "id": "agent-01J9Z3", "name": "screening", "implementation_name": "loan-screening-agent", "approved_versions": ["1.3.2", "1.4.*"], "require_release": true, "...": "the other identity fields"}An invalid pattern (*, 1.*.2, 1.4*) is refused with 422 and nothing is changed.
Patterns are stored sorted and de-duplicated; [] returns the identity to observe-only.
The release timeline:
curl http://localhost:5050/v1/admin/agent-identities/agent-01J9Z3/releases \ -H "Authorization: Bearer $ADMIN_TOKEN"{ "agent_id": "agent-01J9Z3", "implementation_name": "loan-screening-agent", "approved_versions": ["1.3.2", "1.4.*"], "require_release": true, "current_release_id": "rel-01J9Z9", "releases": [ { "id": "rel-01J9Z9", "agent_id": "agent-01J9Z3", "name": "loan-screening-agent", "version": "1.5.0", "build": "sha256:4f1c…", "label": "loan-screening-agent@1.5.0#sha256:4f1c…", "trust": "declared", "first_seen_at": "2026-09-27T09:12:40+00:00", "last_seen_at": "2026-09-27T10:02:11+00:00", "run_count": 14, "fingerprints": ["sha256:9b0e…"], "fingerprint_count": 1, "approved": false, "name_matches": true, "current": true } ], "total": 1}approved is null when the identity approves nothing (observe only). On the
Composition view, each agents[] entry carries
release: {name, version, build, trust, approved, last_seen_at} — or null when the
agent has never declared a release.
Related
Section titled “Related”- Agent identity & policies — who the agent is and what it may do
- Health & conformance — client labels and the assurance report
- Behavioural drift — the cause ladder

