Skip to content

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.

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.

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.

  • On every call (proxy_logs): the resolved name, version, build and carrier (token · mcp_client_info · baggage · header), plus the normalised client label. The raw User-Agent is 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 own clientInfo and 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.

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.

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: high
then:
- set_autonomy: approval_required

These 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):

Terminal window
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:

Terminal window
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.