Why bother with versions at all
An agent isn't just code. The behaviour you ship is the combination of the model name, the system prompt, the skills you bundled in, the MCP servers you wired up, and the container image those things run inside. Any one of them can change underneath you and the agent starts answering differently. Without versioning, you have no way to say "the build that was running yesterday at 3pm" or "the build the finance team certified last quarter".
Concretely, versions are what let you:
- Reproduce a result. When someone says "the agent gave me a weird answer", you need to point at the exact bytes that were running and replay against them.
latestcan't tell you that, becauselatestmoves. - Roll back without redeploying. Last good build is still in the registry under its own tag. Rollback is republishing the pointer your consumers reference, not a code change.
- Run two builds side by side. A canary handful of users on the new version, everyone else on the stable one, decided by which tag each consumer pins to.
- Audit a decision. When a regulator or an internal review asks which version of the agent made a given tool call, the tag on the deployment is the answer. No tag, no answer.
- Separate iteration from promotion. The team iterating on the agent can publish freely; the team consuming it doesn't see those changes until a human moves the pointer tag. Same registry, two velocities.
That's the why. The rest of this page is the how:
the metadata.tag field, a convention for what to put
in it, and the pieces around it (auth from a pipeline, approval
workflow, image digest pinning, deploy-time verification) that
make the whole thing tamper-evident.
What the metadata.tag field actually does
Quick orientation first. You publish things into AgentRegistry
by writing YAML manifests and applying them with the
arctl CLI (or posting them to the registry's REST
API). Each manifest declares an artifact (an Agent, an
MCPServer, a Skill, or a Prompt) in a Kubernetes-style envelope:
an apiVersion, a kind, a
metadata block that names the artifact, and a
spec block that describes it.
apiVersion: ar.dev/v1alpha1
kind: Agent
metadata:
name: summarizer # what to call this artifact
namespace: marketing # which logical group it lives in
tag: 1.4.0 # which version of it this manifest is
spec:
description: Summarises long-form text
# ... source image, model, MCP servers, etc.
metadata.tag is the field on that envelope that
identifies which version of the artifact this manifest
represents. The registry uses the triple
(namespace, name, tag) as the unique key for every
artifact it stores. Two manifests with the same name and
namespace but different tags coexist as separate snapshots.
Two manifests with the same name, namespace, and tag overwrite
each other.
The tag field is optional. Omit it and the server stores the
artifact under the literal tag latest. That default
works fine while you're iterating on your laptop. The minute
another manifest, deployment, or teammate needs to depend on a
specific build, you want an explicit tag with a discipline
behind it.
AgentRegistry treats taggable artifact kinds (Agent, MCPServer,
Skill, Prompt) as immutable per (namespace, name,
tag) tuple. Once you publish
summarizer:1.4.0, those bytes are the canonical
1.4.0. Publish again at the same tag and you
replace the snapshot, which is what makes pointer tags like
stable useful. Mutable control-plane objects
(Providers, Deployments) do not take tags. They're managed by
public name and namespace.
The tag string itself is freeform. AgentRegistry doesn't enforce semver, calver, or any other shape. That's a feature, not a bug, but it means the discipline is on the team to pick a convention up front and stick with it.
metadata.version.
It's been renamed to metadata.tag. Both refer to
the same thing, and the server still maps the absence of the
field to latest.
When to set a tag
If nothing in the registry references your artifact by name,
leave the field out and keep iterating against
latest. As soon as another manifest, agent, or
deployment pins to you, publish an explicit tag and let them
reference that. The cost of running on latest the
whole way through development is zero. The cost of having two
teammates each thinking latest means a different
build is real.
The CLI surface is symmetric: every get and
delete takes a tag selector.
arctl get agent summarizer # latest
arctl get agent summarizer --tag stable # explicit tag
arctl get agent summarizer --all-tags # all tags for that name
arctl delete agent summarizer # latest only
arctl delete agent summarizer --tag stable # one tag only
arctl delete agent summarizer --all-tags # every tag
What to put in the tag (versioning convention)
Because the tag string is freeform, the decision that matters is the convention you pick. The pattern I'd recommend by default is a two-track scheme: an immutable snapshot tag that the build pipeline writes once per build, and a floating pointer tag that consumers reference and that promotion republishes.
Immutable snapshot tag
What the build pipeline writes. One tag per build, never reused.
- Semver (
1.2.3,1.2.3-rc.1) when the artifact has a release cadence and external consumers - Git SHA (
a1b2c3d) when you want maximum determinism and don't need human ordering - Build number (
build-417) when the pipeline is the source of truth and SHA is too opaque
Floating pointer tag
What consumers reference. Republished on every promotion to point at a new snapshot.
stable,prod,canary,dev- Consumers pin to the pointer in their own manifests
- Promotion is a single
arctl applyagainst the new snapshot's bytes - Rollback is the same operation against the previous snapshot
Concretely, the build pipeline publishes summarizer:1.4.0,
then a separate promotion step republishes
summarizer:stable pointing at the same image bytes.
A consumer that referenced summarizer:stable picks
up the new build at the next reconcile. If 1.4.0 misbehaves, you
re-promote 1.3.9 under stable and the consumer
rolls back without redeploying anything else.
Two rules I'd hold to:
- Pick one snapshot convention per artifact kind (semver or SHA or build number) and don't mix them. Mixed conventions across the registry make discovery and rollback painful.
- Don't use
latestas a published reference. It's the default fallback when the field is omitted. Anything a teammate, manifest, or deployment depends on should pin to an explicit tag.
Authenticating from a pipeline
Enterprise AgentRegistry authenticates clients via OIDC. From a
CI runner the shape is: obtain a JWT from your identity provider
(workload identity, client-credentials grant, the OIDC token a
CI system issues to its own runners, and so on), then exchange
it at the registry for a bearer token via arctl user
login. arctl apply uses that bearer for
every subsequent call.
# In the pipeline runner
arctl user login \
--oidc-issuer-url https://idp.example.com/realms/platform \
--oidc-client-id ci-publisher
arctl apply -f agent.yaml
The principle here is short-lived credentials. The pipeline never holds a long-lived password or static API key. It holds an identity (workload identity in the runtime, a client registration in the IdP, or a CI-native OIDC token) and exchanges that for a registry bearer that expires.
RBAC and the approval workflow
RBAC is driven by JWT claims. The role-mapper expression looks at a configurable claim path (group memberships, realm roles, whatever your IdP populates) and maps each caller to a role in AgentRegistry. From there, scoped permissions decide what the caller can read, publish, or promote.
On top of that, AgentRegistry can stage non-admin tagged artifacts for approval before they land in production storage. Combined with the two-track tag convention above, this gives you a clean separation:
| Pipeline action | Tag | Workflow |
|---|---|---|
| Build publishes a new snapshot | 1.4.0 (semver) or a1b2c3d (SHA) |
Lands directly. Snapshot is immutable. |
| Pre-prod promotion | dev or canary |
Lands directly under the same service account. |
| Production promotion | stable or prod |
Staged for human approval before it replaces the live pointer. |
The pipeline service account never needs admin. It publishes
snapshots freely, moves dev and canary
on its own, and asks a human to move stable.
Pinning the image by digest
The Agent's spec.source.image field takes an OCI
reference. Use the digest form rather than a floating tag, so
every arctl pull or downstream deploy resolves to
the same bytes regardless of what's happened in the OCI registry
since.
apiVersion: ar.dev/v1alpha1
kind: Agent
metadata:
name: summarizer
tag: 1.4.0
spec:
description: Summarises long-form text
source:
# Pin by digest, not a floating tag, so the manifest references
# the exact image bytes the build produced.
image: ghcr.io/example/summarizer@sha256:9f1a3c0e5b6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f
Tagging the OCI image with 1.4.0 is still useful
for human readability and for tooling that wants to track which
semver shipped, but the value the AgentRegistry manifest
references should be the digest. A digest is the only form that
can't be silently replaced.
Verifying at deploy time
Cosign-sign the image in the build pipeline, ideally keyless
via the runner's OIDC identity, and the signature lands in the
OCI registry alongside the image. Verification of that
signature happens downstream when the image is about to run.
In Kubernetes that's a policy controller (Sigstore's
policy-controller, Kyverno with its verifyImages
rule, OPA Gatekeeper with a custom constraint), pointed at the
identity that signed and the expected issuer.
The AgentRegistry manifest's job in this picture is small but important: hand the verifier a stable digest. The digest is what the cosign signature is computed over, so a digest-pinned manifest and a policy controller that verifies that exact digest give you an end-to-end chain from build runner to running pod that can be audited and replayed.
Cosign docs cover the actual signing command shape: docs.sigstore.dev/cosign/signing/overview.
Best-practices checklist
Pushing to AgentRegistry, the short version
- Default to
latest. Tag explicitly only when something pins to you. - Pick one immutable-snapshot convention per artifact kind (semver, git SHA, or build number) and stick with it.
- Publish snapshots as immutable tags. Layer floating pointer tags (
stable,prod,canary) on top for promotion. - Never use
latestas a published reference. - Pin the OCI image by
@sha256:digest inspec.source.image, not a floating tag. - Authenticate the pipeline via OIDC (workload identity / short-lived token). No long-lived secrets.
- Use the approval workflow to gate promotion of high-trust pointer tags.
- Sign with cosign at build, verify at deploy with a policy controller pointed at the same digest.