Upgrade a cluster

Review policy changes

Check which calls will start waiting for approval and how policy versions affect outstanding decisions.

For: cluster operators.

Check which calls will start waiting for approval and how policy versions affect outstanding decisions.

Upgrading to v1.0

v1.0 changes one default of the tool policy: functions that declare nothing (no MCP annotations, or not read-only and silent on destructiveHint) now wait for a person's approval at each call; before v1.0 they ran. A tenant that set a policy without setting --undeclared gets the new default too. This covers hooks: a hook whose function declares nothing waits for an approval each time it is asked, and while it waits or when it is denied, it counts as its on_timeout answer, so a guard hook that fails closed stops the call.

Before you upgrade, see which functions it affects, tenant by tenant:

Terminal
oz0 catalog --tenant acme        # the POLICY column says approve, the WHY column undeclared

Then annotate them (the fix, see Moving a plugin to v1.0), or keep the old behaviour while their authors do:

Terminal
oz0 tenant tool-policy set --tenant acme --undeclared allow

Taint and the taint-default rule (v1.1)

From v1.1 every job carries labels, and every tenant's tool policy has the built-in rule rule-taint-default: once a job read outside text (an outside start such as a webhook, a chat message, an A2A or MCP call, a routine fire with a payload, an open-world tool's result, an event), each call of a function that can send data out waits for a person.

Jobs that call open-world tools after reading outside text, which ran unattended before, now wait.

Before you upgrade, decide per tenant: vouch for open-world functions you trust (oz0 tenant labels <tenant> --trusted <tool>.<function>), narrow the rule's pattern or condition, set it to shadow mode to see what it would ask (oz0 tenant tool-policy shadow-report), or turn it off (oz0 tenant tool-policy set --taint-default off --reason ...).

Each of those loosens, so it takes a reason, and a second admin where four eyes is on.

The policy's version changes with the upgrade (the rule is new, and it decides), so approvals that wait across it are asked again.

A harness agent's session (Claude Code, Codex and the like) and a remote agent's session at its provider now taint their job from their start, since their own tools, such as a shell or a web fetch, run inside the session and their results never reach the workflow: the job's calls after the session, and its parent's after its result, are gated as a tainted job's are, so a flow or a lead agent that delegates to a coder and then calls an open-world function now waits for a yes.

The session's own tools are not gated by the rule.

Data a lead passes with oz.delegate reaches the helper's model in a <delegated_data> block that says only whether it may hold outside text, never a level or compartment.

A conversation's next turn after a turn that ran before the upgrade, board notes and files written before it read as untrusted, since what they read is unknown.

Retrying a chat-started job from the console is refused now, as for a webhook's: it runs again only when its sender writes again.

A job whose home must be a tier-1 node (an outside start, such as a webhook's) now follows the tenant's tool policy: before v1.1 such a job ran under the default policy whatever the tenant's rules said, so a rule that refuses or asks now acts on it too.

Approvals that wait across the upgrade are asked again

This release rewords the reason of the built-in rule-taint-default. A rule's reason was then part of the tool policy's version (a later release leaves it out: see the next section), and an approval binds to the version it was asked under, so an approval that waits across the upgrade is asked again, once, about the same call, after the person answers the first ask. Answer the second ask as the first. Approvals asked after the upgrade are not affected.

Rewording a rule asks no waiting approval again

From this release the tool policy's version, which an approval binds to, covers what the rules decide and not the words about them: a rule's reason and rationale, who proposed it and who let it act, the setup assistant's and an install's marks on it, and why the escalation ladder is off are left out.

Rewording any of them while a person decides no longer asks the person again; changing what a rule decides (its action, pattern, condition, deciders, deadline or end, a type's default) still does. The version names its scheme: sha256v2: for this one, sha256: for the one before.

What this upgrade does to approvals that wait across it: an approval asked before the upgrade is bound under the old scheme, which hashed the words too, and it is compared under that scheme after the upgrade. So it is asked again, once, only if what a rule decides changed while it waited, or if the policy's words changed between the two releases.

This release rewords no built-in rule's reason (its new words for the tool policy are in what people read, not in the rules), so the upgrade alone asks nothing again; an admin's rewording while the approval waits does, that once.

Approvals asked after the upgrade are bound under the new scheme. A job whose home node runs an older runtime keeps binding under the old scheme until that runtime is upgraded.

A key decides an approval only when it is named

From this release a tenant API key decides an approval, or answers a question, only when the approval's deciders name it (keys:) or the notification rule whose webhook delivered it names it (--decide-key) (Keys that decide). An integration that decided with any key of a role that could, such as a ticket system's bridge, is refused with 403 after the upgrade.

Before you upgrade, name its key on the rule that sends it the approvals (oz0 notify rule put <name> ... --decide-key <key ID>, a governance change with a reason) or in the deciders. People, apps acting for a person and operators' certificates decide as before.

The tool policy's words and oz0 policy

From this release the console, the CLI, approval cards, the shadow report, errors and the docs speak one vocabulary for the tool policy: Allow, Ask and Refuse for what the policy does with a call (Judge shows on switches; nothing runs it yet), and Read, Write, Sends out, Destructive and Unknown for what a function does. What you may notice:

  • Nothing stored or sent changes. The stored policy, the admin API, every --json output and the old commands' flags keep their names (approve, read_only, a shadow report's deny), so scripts that read JSON or write approve keep working. The tables people read change: oz0 tenant tool-policy show, shadow-report, oz0 catalog (its ANNOTATIONS column is now TYPE) and oz0 plugin policy print Ask where they printed approve.
  • Two types read differently, and decide nothing new. A write that says nothing about destructiveHint shows as Unknown (as the policy decides it: by the Unknown default), where oz0 plugin policy and the install screen grouped it as destructive; --set destructive=... no longer reaches it, and --set unknown=... does. A capability card counts by type too, with Sends out and Unknown counts of their own. An open-world write marked "oz0/class": "write" shows as Write when its plugin is a reviewed listing (installed from the official index under its reviewed namespace), and as Sends out otherwise.
  • oz0 policy is new, beside the old commands, which stay as they were: allow, ask and refuse add one rule, placed as a plugin's switch places it; remove, edit and explain work on one rule; export and apply -f take the whole policy as a file.
  • The text an agent reads when the policy refuses a call names oz0 policy allow <function> instead of oz0 tenant tool-policy set --rule .... A job that started before the upgrade keeps the old text on replay; the old command still works.
  • Approvals that wait across the upgrade are not asked again because of the words: this release rewords no built-in rule's reason and changes no decision.

Verified plugins' writes ask by default, through the plugin's suggested policy

From this release a plugin may suggest its own policy (policy: in its manifest), and the verified plugins suggest that every write asks a person: the install screen and oz0 plugin policy show "Ask, the plugin asks before it writes", which an admin may switch to Allow for a function or the whole group of writes, with a reason (A suggested policy). requires_approval, which no tenant can lower, is kept only for calls a person must decide every time: GitHub's merge_pull_request and push_changes, ServiceNow's add_comment and email's send.

  • When you update a verified plugin, the update writes its suggestions into the tenant's tool policy as rules marked as the plugin's, after your own rules: a rule you have that decides a function still decides it.
  • A function that leaves requires_approval keeps asking: GitHub's close and delete_branch, memory's accept, the Atlassian, Atlassian Data Center and GitLab plugins' writes, and ServiceNow's other writes. The update writes an ask rule for each into the tenant's policy, placed before any rule that would let its calls run (a broad * allow, a rule with a condition or one for some callers), so nothing a person was asked about runs unasked.
    A refusal you have still refuses, and so does a default of yours that refuses: for a plugin without a measured listing, whose function no listing describes yet, the rule refuses it where one of your defaults refuses anything, until you switch it.
    The update's output says which functions and how many rules; switch them with oz0 plugin policy <plugin> --set write=allow --reason ... when you agree.

The edge sends a node the tenant's tool policy with these rules before the plugin set with the new version, reading the policy again first; while it cannot read the policy, the node keeps the versions it runs, under their requires_approval.

  • Writes that ran unasked before ask now where your policy left them to the defaults: GitHub's comments, issues, labels, pull requests and reviews, Slack, Teams and Discord messages an agent sends (which already asked as functions that do not say whether they destroy anything), memory's notes and definitions, and ai-spend's import. A routine that runs ai-spend's daily import unattended needs oz0 plugin policy ai-spend --set oz0_ai_spend.import_spend=allow --reason ... once.
  • A default you tighten later still reaches them: making a default stricter, such as oz0 tenant tool-policy set --destructive refuse, writes a rule with that word before the plugin's rule for each of its functions the default covers, as requires_approval let the default refuse before. Making the default less strict again takes those rules out.
  • What nodes list decides: the server checks the plugin's rules again against the first node's listing of the new version, and narrows a rule that would decide a function more loosely than your policy.
  • Uninstalling a plugin removes the rules it suggested, and keeps the ones you switched.
  • oz0 plugin import openapi writes its writes as asks in policy: instead of requires_approval; --writes-without-approval still leaves them out.
Copyright © 2026