Agent releases
packs:agentspacks:agent-governance
An agent release is the signed, immutable, versioned record of an agent
build: a <name>@<version> identity, the frozen manifest document, the
content-addressed artifact identity (a git sha or image digest) and an
Ed25519 signature over all of it, verified against a registered signing
key before the row ever lands. Releases exist so the fleet can answer —
cryptographically, not by convention — "which build is this instance
running, who signed it, and who promoted it to prod": the signature is
re-verified at every promotion and at every enrolment that declares the
release, so a tampered build can neither climb the stage ladder nor
enrol. Releases are estate-global objects; the agent platform they feed
is opschain docs agent-plane.
Two halves, deliberately separated: signing is local (keygen and
sign run in the build pipeline — the private key never reaches the
server) and verification is server-side (create, promote and
enrolment all re-verify). No surface prints key material or a signature
value — keys are displayed by fingerprint only
(SHA256: over the raw public key bytes).
Signing keys
A release-signing key is a registered Ed25519 verification key: the raw 32-byte public half, active on arrival, with a name, an optional note and audit fields. Ed25519 is the only supported algorithm.
opschain admin agent-release keygengenerates the pair locally and registers the public half; the private key is written to the named file (mode 0600, base64), never sent to the server and never overwriting an existing file (opschain docs cli/admin-agent-release-keygen).opschain admin agent-release keyslists the registered keys, newest first — id, name, lifecycle status, fingerprint, creator, note (opschain docs cli/admin-agent-release-keys).opschain admin agent-release key-statusmoves the lifecycle state (opschain docs cli/admin-agent-release-key-status).
The key lifecycle has exactly three states:
| State | Verifies | Signs |
|---|---|---|
active | Everything its signatures cover. | New releases — only an active key signs (agent signing key is not active: only an active key signs new releases). |
retired | Only releases created before the retirement — historical signatures stay acceptable, so retirement is the rotation verb. | Nothing new. |
revoked | Nothing, at any date — a release signed by it refuses enrolment and promotion immediately. | Nothing. |
Rotation is therefore: register the new key, retire the old. Revocation
is for compromise — it invalidates the key's whole history. Every
registration and status move is audited
(info:agent_release:key_registered,
info:agent_release:key_status_changed).
What the signature covers
The signature is Ed25519 over a deterministic, versioned payload
(opschain.agent_release.v1): the release name and version, the artifact
identity, and the SHA-256 digest of the manifest document. Any
post-signing change to any of them breaks verification — which is what
makes the manifest's contents (the declared agent type, and the
disclosures object the fleet detail and trust review render)
immutable facts of the release rather than registration-time claims.
Produce the signature with opschain admin agent-release sign (offline,
no server call), or let create sign inline from the private-key file
(opschain docs cli/admin-agent-release-sign).
One agent, many instances, releases that move
An agent is the logical identity — the per-project code its service
account, owner, trust grants, policy and recommendations attach to. Its
instances are its running processes: each one a fleet member with its
own keypair, bound at enrolment to the release it runs (the release's
stage cached on the instance and rewritten on every live instance when
the release is promoted, retired or revoked), labelled with a lane. A second state
directory started with the same code enrols a second instance — the
platform never answers already_exists for that; the only
already_exists left is a fresh keypair presenting the id of an instance
that is still live (AGENT_INSTANCE_LIVE). The licence's agents limit
keeps counting connected instances.
Work addresses the agent and the platform picks one instance: by lane
(a dispatch may name one with the X-Opschain-Agent-Lane header; default
stable), then the least-loaded connected instance of that lane whose
cached stage passes admission. The shadow lane receives the same
dispatch as stable and can never write: every change it requests is
forced to the advise tier, and its recommendations carry lane: shadow
— linked to the stable lane's proposal for the same incident (one card,
both provenances), never a second change. That is "test 2.1.0 beside
2.0.0 while production serves" without a second project.
opschain admin agent instances <agent> lists the instances grouped by
release and lane; opschain admin agents list shows one row per
instance.
The stage ladder is the estate's
A release's stage says where it is trusted to act. Stages form an
ordered ladder the estate configures — the default is exactly
dev → test → prod with prod the one governed rung — and a
customer runs its own lifecycle (dev → uat → preprod → prod, say, with
the last two governed) by replacing it:
opschain admin agent-release stages
opschain admin agent-release stages --set dev,uat:governed,preprod:governed,prod:governed
Each rung has a name (lowercase alphanumeric with - and _) and a
governed flag; the ladder needs at least two rungs. Past the top rung
sits the implicit retired state a retired release holds — it is never a
rung of the ladder. The ladder is read through
AgentReleaseService.GetAgentReleaseStages and written through
SetAgentReleaseStages, admin-only and audited
(info:agent_release:ladder_changed, carrying the previous and new
ladders).
A ladder change is validated by name before it lands:
- a rung any release currently sits on cannot be removed, and two
occupied rungs cannot swap order — the refusal names the rung and how
many releases hold it (
agent release ladder change refused: a rung a release currently sits on cannot be removed or reordered); - the estate's
agents.release_stagefloor, every environment'srelease_stage_floorand every trust grant'srelease_stagecondition must still name a rung of the new ladder — the refusal names the floor or grant that would be stranded.
To change a rung's name, rename it in place:
opschain admin agent-release rename-stage uat test
The rename is one set-based write: the ladder, every release at the rung
(each gains a completed promotion attestation old → new with the reason
stage renamed), every pending promotion into or out of it, every
environment floor and every grant condition naming it move together, and
every connected instance's cached stage follows. The estate's
agents.release_stage configuration floor is deployment configuration and
is not rewritten — a rename of the rung it names is refused until the
configuration changes.
The release lifecycle: create → promote
Create (opschain admin agent-release create,
opschain docs cli/admin-agent-release-create) registers one release at
the ladder's first rung (dev on the default ladder). The server
verifies the signature against the named signing key — which must be
active — before the row lands: an unsigned submission, a signature
that does not verify over the manifest and artifact, and a non-active key
each refuse by name, and the refusal is itself audited
(warn:agent_release:refused). A release may freeze a declared agent type
(service or ai) into its signed content. The (name, version) pair is
unique forever: an agent release with this name and version already exists - releases are immutable, publish a new version.
Promote (opschain admin agent-release promote,
opschain docs cli/admin-agent-release-promote) moves a release one
rung up the ladder, re-verifying the signature first — agent releases promote one stage at a time up the estate's ladder - never skipping, never demoting; a target that is not a rung is refused naming the ladder, and
an omitted target means the next rung. Only one promotion may be open per
release (a promotion of this agent release is already held by the approval fabric). Every stage move — requested, completed, rejected or
expired — is a durable promotion attestation on the release,
recording who requested and who decided it; opschain admin agent-release show renders the full attestation history.
Moving instances between releases: the rollover
opschain admin agent rollover <agent> --to <name@version> [--lane stable] [--strategy replace|canary=<n>] [--drain|--drain-only] is the platform
verb that changes a running agent (opschain docs cli/admin-agent-rollover). It refuses unless the target release's
stage satisfies every stage floor the agent's scopes require — the
estate's agents.release_stage, the release_stage_floor of every
environment a trust grant addressing the agent reaches, and every such
grant's own release_stage condition, read off the same compiled trust
snapshot the per-decision refusal reads (see "Where a stage may act"), so
a dev build is never rolled into a lane that serves production — and
unless the release is still serving (not retired or revoked). Then, per
instance of the lane: drain (finish in-flight, no new dispatch — the
routine lifecycle tier), instruct the
restart over the instance's own channel (restart_from_release: state
directory and keypair kept, the artifact fetched and verified against the
release's digest, or — for a containerised instance that cannot replace
its own image — exit code 75 for the container platform to restart it on
the image the release names), re-verify at enrolment as the instance
re-enrols from the new release, resume dispatch. Each of these steps
takes milliseconds on the server's side: the instruction completes the
instance's parked long-poll (or rides its open stream) the moment it is
issued, and once the instance has acknowledged it the server gives its
channel the next turn at once, so the instance says goodbye and restarts
right away instead of up to one poll window (50 s by default) later — the
rollover's pace is the instance's own restart and re-enrolment time. On an
estate of several replicas the instruction is issued by the replica that
holds the instance's channel; a rollover started on another replica waits
for that replica's next sweep (seconds), then proceeds at the same pace.
canary=<n> rolls
n instances first and holds for rollover continue or rollover abort; rollover rollback returns everyone after continue; rollover finish closes the rollover and offers the source release for retirement
— printed, never run. --drain-only drains and reports; the operator
restarts the instances and the rollover completes as they re-enrol (an
instance that returns on an unexpected release stays drained). Each
re-enrolment is recorded on the rollover atomically — instances restarting
together, on one server or on several replicas, never erase each other's
progress — and a verb that reads a rollover another writer has just moved
answers aborted (read it again and retry) rather than overwriting it.
blue-green restarts nothing: a
green lane is brought up on the target release beside the serving
lane and dispatch flips to it atomically. The verb first checks the
licence's agents headroom for the overlap — every instance connected
now plus the green instances still to come (size, default the serving
lane's live count, less those already up) must fit the limit — and
refuses by name with the numbers (licence_headroom) when it does not;
the green lane (<lane>-green by default, never the serving lane or
shadow) must not already hold live instances on another release
(green_lane). The operator then starts the green instances from the
target release with the green lane's name (opschain agent --lane <green> / OPSCHAIN_AGENT_LANE): each enrolment lands on the rollover
through the same atomic append, and at size the rollover holds
green_ready — the green lane serves nothing yet, though a dispatch
naming it explicitly reaches it. rollover continue is the flip: the
green lane is re-verified at size (green_not_ready otherwise), the row
moves to flipped, and ONE set-based write relabels both lanes — the
green instances now carry the serving lane's label and take every new
dispatch, the old instances carry the green lane's name, serve nothing
and complete their in-flight work. The routing admission reads the lane
label, so the flip is atomic for dispatch; a relabelled instance keeps
its label across restarts because a re-enrolment presenting the lane it
presented before keeps the platform's label (one presenting a different
lane is the operator's relabel and wins). rollover rollback is the same
write in reverse and closes the rollover rolled_back; rollover abort
closes it before the flip; either leaves the green lane's instances
running on the target release — stop them, or roll over again and they
count at once. rollover finish retires the old lane: each old
instance is drained, halted with a typed reason naming the rollover (the
process reports the halt and never serves again) and its instance row
retired (instance_retired attested), then the source release is
offered for retirement exactly as under replace. opschain admin agent rollover show renders both lanes and which one serves.
Every step is a durable attestation on the release (rendered by
agent-release show) and an info:agent:rollover_* event on the agent's
path, all under ONE correlation chain. On an estate carrying the approval
fabric a rollover lands held in the same funnel as a governed
promotion (approvers from the request, else agents.approvers) when the
lane serves a governed environment: any environment the agent's grant
scopes reach carries a release_stage_floor at a governed rung of the
ladder (the shadow lane, which never writes, is never held). When no
reached environment carries a floor, the stage stands in for it — a
rollover of the stable lane to a release at the ladder's top rung is
held. Without the fabric it starts directly, audited.
The governed rung
On an estate carrying the approval fabric (opschain docs change-governance), a promotion into a governed rung does not
complete directly: it lands held as a strict checklist in the same
approval funnel governed change enters, its subject naming the target
rung, with a content fingerprint binding every approval to the release
identity, artifact, stages and signing key the approver saw. Approvers
resolve from the promotion request, else from the configured
agents.approvers — an empty resolution refuses, because a governed
promotion is never approver-less. Quorum completion moves the stage
(info:agent_release:promoted); rejection, or the quorum TTL expiring
unanswered, refuses it (info:agent_release:promotion_rejected) and the
stage never moves. The funnel's reviewer verbs — the approvals inbox,
approve/reject, break-glass — apply as they do to any held subject.
Without the fabric the promotion completes directly, audited. On the
default ladder the governed rung is prod.
Where a stage may act
The stage is not only an enrolment check: it is a principal attribute of every running instance, evaluated on every decision. At registration and at every session open the platform reads the enrolled release's CURRENT stage and caches it on the instance's session; a promotion — direct or by quorum —, a rung rename and a revocation refresh that cache on every connected instance of the release at once, so a live session's admission changes on its next request without re-enrolling, and a decision never reads the release row. An instance that enrolled without a release carries no stage and ranks below every rung.
Three controls build on it, from the outside in:
- The estate floor at enrolment —
agents.release_stage(server configuration): a declared release below it cannot enrol at all. Its value must name a rung of the ladder; a floor the ladder lacks refuses every declared enrolment by name until the configuration is corrected. - The environment floor — the
release_stage_floorattribute of an environment (opschain nodes update <project>/environments/<env> --release-stage-floor prod;UpdateEnvironment.release_stage_floor, served on theEnvironmentread shape; needspacks:agent-governance). Every action, run or governed read whose target node sits under that environment — a delegated call under anact_asgrant (run-as owner, asker reads) included — is refused when the agent's release stage is below the floor, or when the agent enrolled without a release and a floor is set — before any trust grant is consulted, so a broad grant never overrides it. The refusal is a422naming the environment, the floor and the agent's release and stage, carries the typed reasonAGENT_RELEASE_STAGE_BELOW_FLOORon the agent lane'sOpschain-Agent-Reasonheader, and lands the auditedwarn:agent:release_stage_refusedevent on the agent's path (agent, release, stage, floor, environment, target path). A floor must name a rung of the ladder; an empty value clears it; every change is audited (info:agent_release:floor_changed). A floor naming a rung the ladder no longer carries is closed, never silently open. - The grant condition — a trust grant's
release_stagefield (opschain admin trust grant … --release-stage prod): the grant admits only instances whose release sits at or above that rung, evaluated where every other grant selector is — in the compiled trust snapshot — so a grant over a production scope can require a production-stage release explicitly even on an environment without a floor. A conditioned grant never admits an instance without a release.
Floors and conditions compare by ladder position, so a custom ladder
works unchanged: on dev → uat → preprod → prod a floor of preprod
admits preprod and prod. The outcome the owner asked for: a dev
build can never act on production nodes by policy, not by convention —
a floor of prod on the production environment plus the funnel-held
promotion into prod means the only builds that ever touch production
are the ones a human approved into that rung.
Retire and revoke: the ladder's end
Beyond the ladder's top rung sits retired (opschain admin agent-release retire <name@version>): the release accepts no new
enrolment and no rollover to it, while instances already running it are
untouched — they keep the rung the release reached and the admission that
rung gives them (retirement is a lifecycle state, never a rank) — the
natural step once a rollover away from it finishes, which is why
rollover finish offers it. Revoke (opschain admin agent-release revoke <name@version> --reason …) additionally halts every instance
running the release through the per-agent halt machinery, each channel
closed with the typed AGENT_RELEASE_REVOKED reason naming the release
(the runtime logs platform: agent release revoked — … and waits), and
drops every live session's cached stage at once — a revoked release holds
no stage, so nothing a floor or condition guards admits it even before the
channel closes; the halt lifts with admin agent halt-resume once the
instance is rolled to a current release. Both are attested on the release and audited
(info:agent_release:retired / revoked); both are one-way (active → retired → revoked); neither touches signing keys — key rotation stays
key-status.
What the fleet refuses at enrolment
An agent manifest that names a release (release_name and
release_version — both or neither: an agent release is named by both release_name and release_version) is verified at registration, before
the member enrols. The typed refusal ladder, each one audited as
warn:agent_release:enrolment_refused on the agent's own path under its
project:
- unknown — no release holds the (name, version);
- unsigned — the row carries no signature;
- tampered — the signature does not verify over the stored manifest and artifact (including a signing key that is no longer registered), or the key is revoked, or it was retired before the release was created;
- wrong type — the manifest's declared type differs from the type frozen in the signed release (a signed release cannot be misdeclared);
- below the stage floor — the release's stage is below the estate's
configured
agents.release_stage(or that floor names a rung the ladder lacks); - retired or revoked — the release's lifecycle no longer serves enrolment.
A verified enrolment binds the release id to the instance — each
running process of the agent carries its own release and the release's
stage, so two instances of one agent may run two releases — and a
disclosures object frozen in the signed manifest becomes authoritative:
a registration declaring a differing set refuses by name — ship a new
release to change what an agent discloses. An agent that names no release
enrols exactly as before; the stage floor constrains declared releases,
it does not force declaring one. The registration-side detail lives on
opschain docs agent-plane.
Settings
| Key | Where | Effect |
|---|---|---|
agents.release_stages | the release registry (admin agent-release stages) | The estate's ordered stage ladder with per-rung governance; default dev → test → prod, prod governed. |
agents.release_stage | server configuration | The minimum rung required of a declared release at enrolment. Empty requires none — a declared release still verifies. Must name a rung of the ladder. |
agents.release_stage_floor | each environment (nodes update --release-stage-floor) | The rung an agent's release must hold to act under that environment; empty means no floor. |
agents.approvers | server configuration | The fallback approver set for a funnel-held promotion into a governed rung (and for supervised agent-created work generally). |
The configuration keys are catalogued on opschain docs config; every key
is in the per-key dictionary on opschain docs settings-reference.
Governance and audit
Every AgentReleaseService procedure sits behind the /admin
updatable determination plus the packs:agent-governance
entitlement (see entitlements); the release catalog's
reads (list/get) are additionally admitted to principals readable on
/governance/agents, so review surfaces can read without admin
standing. On licence lapse the admin surfaces answer the pack's typed
refusal, while verification itself — the enrolment gate over an
already-declared release — keeps running: the gate is base engine, the
administration is the pack.
The events, all customer-visible on the platform path (lifecycle) or the
agent's path (enrolment refusals), catalogued on
opschain docs events:
info:agent_release:key_registered/key_status_changed— the key registry moved.info:agent_release:created— a signed release landed atdev.info:agent_release:promoted— a stage moved (direct, or by quorum completion).info:agent_release:promotion_held— a promotion into a governed rung entered the funnel, with its fingerprint and quorum deadline.info:agent_release:promotion_rejected— a held promotion was rejected or its quorum TTL expired; the stage never moved.warn:agent_release:refused— a create or promote refused (unsigned, tampered, non-active key).warn:agent_release:enrolment_refused— an enrolment declaring a release was refused by the verification ladder.info:agent_release:ladder_changed— the stage ladder was replaced or a rung renamed (previous and new ladders; the rename and the number of releases attested).info:agent_release:floor_changed— an environment's release-stage floor was set or cleared.warn:agent:release_stage_refused— on the agent's own path: an action, run or governed read was refused by an environment's floor (agent, release, stage, floor, environment, target path).info:agent_release:retired/info:agent_release:revoked— the release lifecycle moved (a revocation names how many instances it halted).- On the agent's path:
info:agent:rollover_started,info:agent:instance_drained,info:agent:instance_restarted,info:agent:canary_held,info:agent:green_ready,info:agent:rollover_flipped,info:agent:rollover_continued/rollover_aborted/rollover_rolled_back/rollover_finished, andwarn:agent:rollover_refusedwith its typed reason.
Related pages
opschain docs cli/admin-agent-release— the command family:keygen,keys,key-status,sign,create,list,show,promote,retire,revoke,stages,rename-stage, each with its own manual page.opschain docs cli/admin-agent-rolloverandcli/admin-agent-instances— moving instances between releases, and seeing them.opschain docs resources/environment— therelease_stage_floorattribute;opschain docs resources/trust— the grant'srelease_stagecondition.opschain docs agent-plane— the platform the releases feed: registration, manifests, disclosures, trust.opschain docs change-governance— the approval funnel that holds the prod promotion.opschain docs events— the event catalogue.
The API surface is AgentReleaseService (see
opschain docs api-v2): RegisterAgentSigningKey,
ListAgentSigningKeys, SetAgentSigningKeyStatus, CreateAgentRelease,
ListAgentReleases, GetAgentRelease, PromoteAgentRelease,
RetireAgentRelease, RevokeAgentRelease, GetAgentReleaseStages,
SetAgentReleaseStages, RenameAgentReleaseStage; the rollover rides
AgentService (ListAgentInstances, RolloverAgent,
AdvanceAgentRollover, GetAgentRollover).