Upgrade a cluster

Prepare plugins for an upgrade

Some server releases require a plugin update first. Check the named versions before upgrading.

For: cluster operators.

Some server releases require a plugin update first. Check the named versions before upgrading.

Lockfiles and hash pins

From the release with lockfile checks, the edge refuses to install or update a plugin whose own Python project has no uv.lock, or whose Node.js project has no package-lock.json, and it pins every pypi: and npm: package by the hashes of its files, with every package it depends on (lockfiles, pinning and hashes).

Plugins installed before keep running on every node as they are: their bundles carry no hashes, and nodes install them as before.

Only their next update is refused until the plugin commits its lockfile (uv lock, npm install --package-lock-only).

An npm: package with a range (@^1.2) is refused at its next install or update; pin one version.

The edge reaches the package indexes at every install of such a plugin now, not only for unpinned packages: point it at your mirrors with OZ0_PLUGIN_PYPI_URL and OZ0_PLUGIN_NPM_URL if it may not reach pypi.org and registry.npmjs.org.

Eval recordings and the status block

From the release with the status block, the runtime ends each model request of an agent's loop with a status block of the budget, steps and time used. A plugin's eval recordings made before it hold requests without the block, so plugin test warns that the first call of each case differs from the recording, and answers it from the recording as before.

Record the cases again once (plugin test --record) to bring them up to date. From then on a replay leaves the block out when it compares a request with its recording, so a replay that takes longer than the recording still matches.

Manifests: one part on its own, addresses and requires.oz0

  • A SKILL.md at a folder's root loads as an Orchestrator Zero plugin. Until this release, a folder with only a SKILL.md at its root imported as a Claude Code skill, with its text untrusted. Now a root SKILL.md without Claude Code's folders beside it (skills/, commands/, agents/, hooks/hooks.json, .mcp.json) is a plugin of that one skill, named after it, with a generated manifest (A plugin that is one part).
    One installed before keeps running as it is. Its next update loads it as the new kind of plugin; a skill from a marketplace was named after its marketplace entry, and is now named after the skill's own name:, so where the two differ, install it again under the new name and remove the old plugin.
  • requires.oz0 is enforced. A plugin whose requires.oz0 the running release does not satisfy is refused at lint and install: "this plugin needs Orchestrator Zero >=1.2; this is 1.0.0". A development build satisfies every constraint.
  • References across plugins name their plugin. A bare name of another plugin's function or agent still resolves, and plugin lint warns (OZ-S019); verified plugins fail lint on it. Write the address, github/oz0_github.get_file, or run plugin lint --fix. Plugins written with bare names keep running: nodes get the same bare names as before.
  • runtime: is optional for a plugin without code of its own, and a Markdown file can be an agent. Neither changes a plugin you have.

Addresses in commands, the APIs and tool policy rules

Everywhere a part is named after its plugin is installed, its address works beside its bare name, and the server checks it against the part's owner:

  • Commands and APIs take addresses. oz0 run, oz0 chat, oz0 tool call, oz0 kill agent|tool, oz0 inventory set, oz0 job search --agent, oz0 policy and oz0 tenant tool-policy set --rule, and the runtime API's StartJob, CallTool, GetCatalog and ListJobs, take <plugin>/<part>. An address that names another plugin is refused with the owner found (github/triage is not an agent of github: triage is support-triage's). Bare names work as before: nothing you run today changes.
  • What people read shows addresses. oz0 catalog, oz0 job get, oz0 job trace, oz0 approval list, oz0 inventory list, oz0 plugin inspect and the console show agents, flows and functions as <plugin>/<part>. The JSON outputs keep their fields and add address beside them; a script that reads name or agent still gets the bare name.
  • A tool policy rule may name its plugin. A rule written github/* decides only github's functions; the policy stores the plugin apart (plugin, beside match).
    A policy without such rules is stored, sent to nodes and versioned byte for byte as before.

A node of an earlier release ignores the field, so the edge sends such a node what it can read: a rule that allows or asks by address with a wildcard in its tool part (github/*, github/*.get*) goes with one copy for each of the plugin's tools (oz0_github.*), so that node decides github's calls as the rule says and every other plugin's calls by your later rules and the classes, exactly as an upgraded node does.

The copies follow the plugin's tools: an update that adds a tool sends them again.

A rule that names a whole tool (github/oz0_github.*) needs no copies, and a refuse rule by address reaches every plugin's matching functions on such a node, which only refuses more until you upgrade its runtime.

The copies show in a node's policy file and decide nothing on an upgraded node; the reason an older node gives names the copy (the tenant's rule oz0_github.*), with the rule's ID.

  • Two plugins that define one agent or flow name run neither, at the edge and on every node, where the edge used to start the first one and a node ran the last. Installs have refused a second one for several releases; oz0 catalog shows such a pair.
  • Model-visible function names over 64 characters (<tool>__<function>) are left out on a node, since OpenAI's formats refuse them; plugin lint warns about one in a plugin's measured listing.

What an install prints, and switches the server places

  • plugin install and plugin update print three more blocks after the install: what the plugin needs, what it may do and what it reaches. They ask only when their input, output and errors are all a terminal, without --json or --yes: then they show the blocks first and ask once, so an interactive install now waits for a yes.
    Anywhere else they ask nothing, as before, and that includes a script started from a terminal whose output is captured or redirected.
    A script you run by hand whose oz0 plugin install writes straight to the terminal passes --yes, or sets OZ0_YES=1 once at its top (export OZ0_YES=1), which does the same for every install and update in it. Their --json answer gains screen; nothing else in it changes.
  • oz0 plugin policy --set places its rules on the server (SetToolSwitches), with the same placement as before, so the rules it writes are the same. Against a server older than this release it places them as it did.
  • An update brings back a stricter default for a function whose type rose since the installed version, as an ask rule written at the update (a tightening, audited as a change of the tool policy). A tenant that allowed such a function asks again until an admin switches it back with a reason.

Updates are checked against the plugins that use them

From this release, installing or updating a plugin, and its quality gate's pass and its rollout's start and end, are checked against every installed plugin whose uses: names it: a version outside a dependent's range, or without a function, agent or skill a dependent is granted, is refused with the list (updates and the plugins that use them).

Nothing installed changes at the upgrade. A script that updates plugins and meets the refusal either updates the dependents first or passes --force --reason <why>, which the audit log keeps as plugin.update.forced. A rollout or gate already running at the upgrade is checked at its end. ai.oz0/pr-reviewer now takes ai.oz0/github from 1.1 (github 1.0 has no download_archive, which it calls).

Event sources: what a node answers is held to the delivery

From this release the edge checks what an event source's function answers against the delivery it verified, by the fields the source binds. After the upgrade:

  • Update the chat plugins (Slack, Teams, Discord) and the event-source plugins (GitHub, GitLab, ServiceNow, Atlassian Data Center) to their versions of this release: a chat channel acts only on a message whose author, room, button, text and chosen agent its source binds, and refuses the messages of an older plugin, saying why. The Essentials set's new version pins them.
  • A routine, flow or wait that names a type, a subject, a filter or a sender fires only on fields its source binds. A third-party source that binds nothing still records its events, gives one event a delivery, and lint warns.
  • A delivery's job is named by its body's digest (sha256-...), not by the sender's delivery header, which is kept as sender_delivery in its record: a body sent again starts nothing new. A flow or routine webhook that sends the same body again on purpose signs X-Oz0-Timestamp.
  • A Slack form left open across the upgrade is refused when it is sent: open it again from its message.

Event sources: bound values compare by type and presence

From this release a bound value is compared with its type: false, 0, "" and null are different values, a number is not its digits, and an attribute a binding gives as null must be left out of the event. A value that cannot be compared refuses the delivery.

  • Update ai.oz0/github to 1.5.1 and ai.oz0/gitlab to 1.2.1 before you upgrade the servers (oz0 plugin update github, or the Essentials set's 1.1.1). Their earlier versions bind "", 0 or false for attributes their functions leave out, so an upgraded edge refuses every delivery from them, each recorded as event.failed, and no routine, flow or wait fires on them until the plugin is updated. The new versions pass on servers of the release before, so updating first loses nothing. A plugin you copied or forked from examples/github-events needs the changes of its 1.1.1.
  • A third-party source whose bindings say '', 0 or false for an attribute its function leaves out fails the same way. Make such a binding give null. A field the provider sends either as a value or as null is named in the binding, as cel.bind(v, body.?x.orValue(''), v == null ? '' : v).
  • While edges of two releases run side by side, a tool call whose short arguments hold an integer no double holds (2^53 + 1, say) can be refused its connection's token, since the old edge rounded the integer and the new one does not. It passes once every edge is upgraded. An approval on such an integer that waits across the upgrade keeps the digest it was asked with.
Copyright © 2026