Skip to main content
Version: v5.7.0

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 keygen generates 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 keys lists the registered keys, newest first — id, name, lifecycle status, fingerprint, creator, note (opschain docs cli/admin-agent-release-keys).
  • opschain admin agent-release key-status moves the lifecycle state (opschain docs cli/admin-agent-release-key-status).

The key lifecycle has exactly three states:

StateVerifiesSigns
activeEverything its signatures cover.New releases — only an active key signs (agent signing key is not active: only an active key signs new releases).
retiredOnly releases created before the retirement — historical signatures stay acceptable, so retirement is the rotation verb.Nothing new.
revokedNothing, 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_stage floor, every environment's release_stage_floor and every trust grant's release_stage condition 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:

  1. 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.
  2. The environment floor — the release_stage_floor attribute of an environment (opschain nodes update <project>/environments/<env> --release-stage-floor prod; UpdateEnvironment.release_stage_floor, served on the Environment read shape; needs packs:agent-governance). Every action, run or governed read whose target node sits under that environment — a delegated call under an act_as grant (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 a 422 naming the environment, the floor and the agent's release and stage, carries the typed reason AGENT_RELEASE_STAGE_BELOW_FLOOR on the agent lane's Opschain-Agent-Reason header, and lands the audited warn:agent:release_stage_refused event 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.
  3. The grant condition — a trust grant's release_stage field (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​

KeyWhereEffect
agents.release_stagesthe release registry (admin agent-release stages)The estate's ordered stage ladder with per-rung governance; default dev → test → prod, prod governed.
agents.release_stageserver configurationThe 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_flooreach environment (nodes update --release-stage-floor)The rung an agent's release must hold to act under that environment; empty means no floor.
agents.approversserver configurationThe 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 at dev.
  • 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, and warn:agent:rollover_refused with its typed reason.
  • 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-rollover and cli/admin-agent-instances — moving instances between releases, and seeing them.
  • opschain docs resources/environment — the release_stage_floor attribute; opschain docs resources/trust — the grant's release_stage condition.
  • 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).