Reviewed changes

Review changes to protected paths

Review changes to protected paths for tenant admins and governance reviewers.

For: tenant admins and governance reviewers.

Protected paths

What judges a plugin, and what sets its rules, is protected from the agents that change it: no agent can make a plugin look better by weakening its tests. A changeset, a skill proposal or a push of changes that touches any of these needs either a person's own change, or a second approval by a person who holds the governance permission:

KindWhatWhere
TestsThe plugin's evalsEvery folder and file its manifest's evals: lists, and every eval file the loader finds there
TestsWhat its evals run under and replayThe tool-policy file an eval's policy: names, the flows' recorded steps (steps/ beside each eval file) and the agents' recorded model answers (evals/cassettes/)
TestsIts agents' outcome checks, best-of judges (an agent's own, its delegations' and a flow step's) and quality samplingTheir rubric_file, verify_file and anchors_file, wherever they are
TestsDetailsDetails
TestsHeld-out checksNone yet: when the platform keeps hidden checks, their files join this list
RulesThe manifest's evals:, policy: and hooks:, and each tool's requires_approval and approval:Inside oz0-plugin.yaml, key by key; an imported plugin's hooks in .claude-plugin/plugin.json
RulesThe code the plugin's hooks runEvery file or folder the command or environment of a tool that a hook calls names; an imported plugin's hooks/hooks.json and the files its hooks' commands name
RulesPolicy filesAny YAML or JSON file of the repository whose format: is oz0.policy/v1 (oz0 policy export) or oz0.plugin-config/v1 (oz0 plugin config export); the GitHub connector, which reads no YAML itself, counts any file that mentions either format
RulesCODEOWNERSCODEOWNERS, .github/CODEOWNERS, docs/CODEOWNERS and .gitlab/CODEOWNERS
RulesCI workflows.github/workflows/, .github/actions/, .gitlab-ci.yml, .gitlab/ci/, .circleci/, azure-pipelines.yml, .azure-pipelines/, .buildkite/, Jenkinsfile, .travis.yml, bitbucket-pipelines.yml, .drone.yml, .woodpecker/, .woodpecker.yml, .forgejo/workflows/ and .gitea/workflows/

Protected test configuration

What

The outcome:, best_of:, quality: and failures: blocks of every agent, its delegation:'s best_of:, and each flow step's best_of:

Where

Inside the agent's or flow's definition (YAML, or a Markdown agent's front matter), compared agent by agent and flow step by flow step, key by key: the file itself may change, these blocks may not. Every definition file the manifest lists is read as it is written, also one the node's loader leaves out (a key it does not know yet, a value it refuses), so a block is compared before the node knows it.

An agent moved to another file, renamed or removed, a name defined twice, a definition that does not parse, or a manifest that lists other agent files, counts when it could drop or change them; an agent moved as it was does not

A changeset's protected paths are found in its own tree: every folder that holds an oz0-plugin.yaml or a .claude-plugin/plugin.json is a plugin, read as it was before the change and as it is after, and a changed path that falls in either's tests or rules counts. CODEOWNERS, CI workflows and policy files count in every changeset, whether or not the repository holds a plugin.

A plugin whose manifest does not load on either side, a plugin too large to compare, or a node that cannot run the check makes every changed path below it count, so the check fails closed. A skill proposal never touches any of this, nor the plugin's other definitions and code: the node refuses it.

What it waits for. The changeset's changes approval comes first, as for any changeset, and its card says what the change is: this change touches the plugin's tests or its rules, with those files listed first. review: policy never approves it. Once a person approved the changes, the job asks a second approval of its own kind, protected_paths, of the same apply call and the same patch. Only a person decides it who:

  • holds governance.write in the tenant. There is no governance role: tenant admins and owners hold the permission, and a custom role may hold it;
  • is signed in to OZ themselves: never an API key (even one a rule names), an app or an MCP client acting for them, a chat channel, an operator certificate, an agent or a policy;
  • neither started the job (nor created the key that started it) nor approved its changes (nor created the key that approved them, even after revoking it; a key whose record cannot be read refuses the approval; so does a moment when who started the job cannot be read, and the person decides again once it can).

Anyone who may decide the job's approvals may deny it. This rule is the platform's, not a tenant setting: no tenant rule, duty rule, four-eyes setting or tool-policy rule loosens it.

The tenant's duty rules are another thing: they say which functions one agent may hold together, the tenant writes them, and a pair may flag or refuse. This rule says who approves a change to what judges a plugin, and it is fixed. The two share the words flag and refuse, and nothing else.

oz0 changes approve <job> decides whichever of the two approvals waits, and oz0 changes show lists the protected files first:

Job job-0193...'s changeset: proposed
  this change touches the plugin's tests or its rules (1 files): it needs a second approval by a person with governance.write
    tests  plugins/notes/evals/e1.yaml: the plugin's evals (plugin notes)
  decided by user:kevin

A person's own change. A rollback a person signed in to OZ asks for (oz0 changes rollback) is that person's own change: it waits for its changes approval only. One a key, an app or an MCP client asks for puts back protected paths only after the second approval, as the change it reverses did; when the change it reverses was never checked (it predates the check), the apply function refuses what lies in a plugin's folder.

The apply function checks too. The call that applies the changeset tells its function what the platform checked. oz0_github.push_changes refuses, before it writes anything, a patch that touches the plugin's tests or rules without the second approval; a patch that touches CODEOWNERS or a CI workflow, outright, unless the second approval names exactly those files; and a patch of a plugin's files, or of a policy file, that the platform did not check (a direct call, or a changeset taken by a node that did not run the check, such as an older one during an upgrade).

The approval lists at most 50 protected files, CODEOWNERS and CI workflows first; a changeset that changes more CODEOWNERS and CI files than that is refused whole, so split it. GitLab's connector has no push of changes yet; when it gets one, it makes the same checks.

A change budget per plugin

Reviewers who approve one agent-made change after another stop reading them. Each plugin therefore has a change budget: at most a number of changesets that agents propose or apply to it in a period, and a cooldown after a rollback of it.

It counts every agent-made changeset to the plugin: a harness agent's changes: whose job input names the plugin (the field changes.target names), and every skill proposal into it. A person's own change and a rollback are not counted.

A job counts a changeset against its own agent's plugin, or a plugin whose results the tenant let that plugin read (plugin reads grant <plugin> results --target <target>): a changeset that names any other target is refused before it is counted, so no job spends another plugin's budget.

PartDefaultWhat it does
max3At most this many agent-made changesets proposed or applied in the period; 0 allows none
period7dThe window the count runs over, ending now: 1h to 90d
cooldown72hHow long new changesets are refused after a rollout of the plugin rolls back or is aborted, or someone asks to roll back one of its changesets with oz0 changes rollback: 0 (none) to 30d

What is counted. A changeset counts when its approval is about to be asked (proposed) and when its apply call succeeds (applied), once either way. A denied changeset still counts: it took a reviewer's time. A merge on the forge happens outside the platform, so the budget counts what was proposed or applied in the period, not what was merged. Each counted changeset keeps its job, its agent, its time and its outcome (waiting, denied, applied, failed, rolled back), never a person.

When it is spent. Before its approval is asked, a new changeset is refused when the budget is spent, when a cooldown runs, or at the floor (below). Nobody is asked: a harness job ends with the reason (the changeset was not offered for approval: plugin notes's change budget is spent: 3 agent-made changesets were proposed or applied in the last 7d, and it allows 3), its changeset stays in its memo as refused for anyone who wants to read it, and a proposing agent is told why.

A change_budget finding about the plugin opens, with the reason, the budget and the count; the next changeset the budget allows resolves it.

Terminal
oz0 plugin change-budget notes                    # the budget, the count, a cooldown, the floor, the latest changesets
oz0 eval findings --kind change_budget --state open

Rubber-stamping tightens it. While an approval_pattern finding is open for the queue that decides the plugin's changes (the tenant's queue, or a team the apply function's deciders name), the budget drops to its floor: one changeset waiting for a decision at a time. Both findings name each other (related in oz0 eval findings --json), and resolving the pattern gives the budget back.

The pattern is about a queue, never a person, and goes through works-council mode as before. A changeset waiting for its decision holds the floor however long it has waited, even past the period; one whose job ended while its approval waited holds it no longer. The Configure tab names a team queue's pattern once it has refused a changeset.

One job, a few changesets. One job counts at most 20 changesets, refused ones included; past that the edge refuses the next without counting it, so a runaway or misbehaving job cannot spend the budgets of other plugins. A step that fails and tries again is counted once, and is decided again when its first attempt never got a decision.

Changing it. The budget is a part of the plugin's configuration, agent_changes:, on its Configure tab or in oz0 plugin config apply. It is a governance setting: allowing more changes, counting them over a shorter period or shortening the cooldown loosens it, so the change takes a reason and another admin approves it. A plugin removed and installed again keeps its budget and its count.

The job never changes its own check

An outcome check's rubric and verify_file come from the agent's definition as it was when the job started, pinned in the job's history: a session that rewrites a verify script or a rubric in its work directory is still judged by the originals, and the job's trace shows the pinned check (outcome check pinned). A session whose plugin's outcome files would lie inside a folder it may write does not start, and a changeset that touches them is refused.

For an agent with changes:, the changeset is taken first and the verify script runs afterwards, on that same tree, so what lands is what the check started on. If the script changes a file of the changeset or of its base (a test that rewrites the source once its assertions have passed), the changeset is refused, because what passed is not what it holds.

Caches a test run writes (__pycache__, .pytest_cache, .mypy_cache, .ruff_cache, .hypothesis, .cache) and new files of its own do not count. Whatever the script leaves running is killed when it exits, and only the last 4 KiB of its output are kept.

Copyright © 2026