---
title: "Review and roll back changesets"
description: "Review and roll back changesets for tenant admins and governance reviewers."
canonical_url: "https://docs.oz0.ai/govern/reviewed-changes/changesets"
---
# Review and roll back changesets

> Review and roll back changesets for tenant admins and governance reviewers.

For: tenant admins and governance reviewers.

## Changesets

An agent that writes files declares where its changes go instead of writing to its target itself:

```yaml [agents/acme-fixer.yaml]
name: acme-fixer
runtime: harness:claude-agent-sdk
input_schema: schemas/fix.json        # {task, repository, sha, archive}
model:
  primary: anthropic/claude-sonnet-5-5
instructions: prompts/acme-fixer.md
harness:
  tools: [Read, Glob, Bash]
changes:
  review: required                    # or policy, below
  apply: oz0_github.push_changes      # what applies an approved changeset
  arguments: {branch_prefix: "fix/"}  # more arguments of that call, fixed here
  base: archive                       # the input field with the repository's archive (oz0_github.download_archive)
  commit: sha                         # the input field with the commit it is of
  repository: repository              # and the repository
  target: plugin                      # the input field naming the plugin it changes, for the change's record
  evidence: evidence                  # and the one citing what it rests on: ["finding:<id>", "comparison:<id>"]
```

```yaml [oz0-plugin.yaml]
uses:
  github:
    tools: [oz0_github.push_changes]  # another plugin's function needs a grant, as any call does
```

1. Before the session, the runtime unpacks the archive the job's input names into the session's work directory and keeps a copy of it apart, as the base. A `.git` the session makes there later is left out and never replaces that base. Without `base`, the work directory starts empty, or the session clones into it: a git checkout's base is the commit the job's input names under `commit`, or else the commit the checkout started from.
2. When the session ends, the runtime computes the diff of the work directory against the base: every file added, changed or removed, in git's patch format, binary files included, at most 2,000 changed files of at most 4 MiB each and 8 MiB in all.

Files that did not change are compared by their git object names, so a large file nobody touched does not count; only a file of the same size as its base is hashed, at most 64 GiB is compared this way, and a changeset that would take more is refused.

The session's files are read as what they are, the session's: never through a link and only regular files, so a FIFO cannot hold the node, and a file over a limit is refused by its size before any of it is read.

A sparse file of the same size as its base still costs the time to hash it, which is what the 64 GiB bound is for. It stores the patch as a file by its digest.
3. The job asks for a **changes approval**, which the Approvals page shows with the diff. It is bound to the apply call's arguments, which name the patch by its digest: a decision on another patch is refused.
4. On a yes, the job calls the function with the repository, the base and the patch. `oz0_github.push_changes` applies the patch onto the base commit as one commit on a branch of its own and opens a **draft pull request**. Every file's base must be the file the patch was made against, or nothing is applied.

```bash [Terminal]
oz0 changes show job-7c1f...      # base, files, size, state, who decided, the pull request
oz0 changes diff job-7c1f...      # the patch, for git apply or a review
oz0 changes approve job-7c1f... --patch sha256:...   # only if it is still the patch you reviewed
oz0 changes reject job-7c1f... --comment "not like this"
```

A changeset that touches a [protected path](https://docs.oz0.ai/concepts/security) (`.git/hooks`, `.git/config`, `.git/config.worktree`, `.git/commondir`, `.claude`, `.codex`, `.cursor`, `.vscode/tasks.json`, `.envrc` and the agent's own `sandbox.protect`), anything inside a `.git` folder, or one of the plugin's own outcome files (below) is refused and never offered. So is one too large.

These protected paths stop a session from **writing** what would run code later; they are not the [read-protected credential paths](https://docs.oz0.ai/concepts/security/protected-files#limits-protected-paths-and-file-writes) (`~/.ssh`, the cloud command lines' credentials and the like), which stop a session from **reading** a credential, and which a changeset does not touch: a secret the change carries inside the working directory is [reported on the job](https://docs.oz0.ai/concepts/security/identity-and-secrets#secrets-in-the-working-directory), not refused.

Seeing a diff takes the permission to see the tenant's content; operators, admins and owners decide. `oz0 changes show` prints the patch's digest; `--patch` with it makes `changes approve` refuse when the job asks again about another patch, rather than approve one you did not see. The console does the same: its **Approve** button is offered only while the waiting approval is for the patch the diff card shows.

### A policy instead of a person

```yaml
changes:
  review: policy
  policy:
    paths: ["docs/**", "README.md"]   # every changed path matches one: * within a segment, ** for any depth
    max_files: 20
    max_bytes: 200000
    executables: false                # a new executable bit or symbolic link goes to a person (the default)
  apply: oz0_github.push_changes
```

A changeset that keeps to every rule is approved by the policy (the job's memo says `decided by policy`); any other waits for a person, with the rule it broke. A policy names what it approves: `plugin lint` refuses one without both `max_files` and `max_bytes` (each at most the platform's own bound, 2,000 files and 8 MiB), and a path glob that names no folder or file.

A glob names one when one of its segments is a whole folder or file name, with no wildcard or `[...]` class in it (`docs/**`, `README.md`), or when its last segment ends in an extension written out after its last wildcard (`*.md`, `**/*_test.go`). Wildcards, classes and punctuation alone (`**`, `*`, `**/*`, `**/*.*`, `**/[a-z]*`), or letters between wildcards inside one segment (`**/*e*`, `**/*s`, `**/x*/**`), would match every path or a large share of any repository.

A changeset that touches a plugin's tests or rules always waits for a person, and then for a second one ([protected paths](https://docs.oz0.ai/govern/reviewed-changes/protected-paths#protected-paths)), whatever the policy says. The policy stands in for the apply function's own `requires_approval` only: when the tenant's [tool policy](https://docs.oz0.ai/govern/tool-policy) asks a person to approve that function (a rule, a kind, or a rule with a condition that holds for the call), the changeset waits for a person whatever the agent's policy says.

### Rollback

Each applied changeset records its pre-image: the base commit and the digests of the files it replaced. `oz0 changes rollback <job>` reverses its patch and proposes the inverse as a tool job of its own, applied by the same function to the target's default branch as it is then (for GitHub, a second draft pull request that reverts the first, whether that one was merged with a merge commit, squashed or rebased).

Every file the inverse touches must still hold what the first apply wrote, so a rollback applies only once the first change is merged and nothing has changed those files since; a pull request that was never merged is closed, not rolled back.

It waits for its own changes approval, with its diff, as the first one did.

The function, the repository and the arguments a rollback uses are those of the call the job's history shows applied the patch, never what the job's record says of itself: when that record names another function, repository or commit, or no call applied the patch without an error, the rollback is refused and the change is reverted where it landed.

### Skill proposals

An agent with `skills: {propose: ...}` proposes a skill with `oz.propose_skill` (see [Skills an agent proposes](https://docs.oz0.ai/build/parts/skills/usage-and-proposals#skills-an-agent-proposes)). A proposal is a changeset of one skill's folder of the plugin it names, built from the tool call rather than a work directory, so native agents propose too.

It always waits for a person: there is no policy for it, and its approval cannot be edited. A job proposes one skill, so the changeset its approval names is the one every review shows.

`oz0 approval list` marks it `SKILL-PROPOSAL`, the approval carries the job's taint reasons when it has any, the console's changeset card names the skill and the plugin and warns when the job read untrusted input, and `oz0 changes show` says what it is:

```text
Job job-0192...'s changeset: proposed
  a skill proposal: skill triage for plugin acme-support
  from a job that read untrusted input: read every line before you approve
  acme/support-plugins at default; 2 files, 1214 bytes, +9 -2
```

Its base is `default`, the repository's default branch when the person approves; the pull request's title and body are the platform's, with the job, its agent and version, its taint first and its labels.

A changes approval follows the tenant's tool-policy rule for its apply function, as a tool approval of that function does: the people it names decide, and `not_starter` keeps the job's starter from approving.

While four eyes are on, the starter of a job never approves its skill proposal alone, whatever the rules say; when the edge cannot read the tenant's setting, it treats four eyes as on.

A job started from a chat thread that runs as its binding's service identity was asked for by the chat's author: four eyes refuses the account they are mapped to as the starter, from the thread's buttons, the console and the API alike, and the approval card says the starter may not approve.

A turn that someone's reply started in a thread another person began was asked for by both: four eyes refuses each of them on that turn's approvals, on every route, as the thread's buttons do.

Each is refused by the account their chat identity is mapped to when the approval is decided, so a person who links their chat identity to an account after the turn started is refused as well; when the edge cannot read that mapping, it refuses the approval until it can.

### Where a change came from

Every changeset an agent makes carries a record of where it came from, which the platform writes and the model never does. Before the changeset's approval is asked, the job records:

- the job, with a link to its page;
- the agent, its plugin and that plugin's version;
- the model alias the agent asked for and the model the job resolved it to;
- the plugin the change is to and its version, when the definition's `target` names it (a skill proposal's is its plugin);
- the evidence it rests on, each by its ID: the evidence reads the job's tree made (`oz.evidence`, by their audit entries), and the findings, eval runs and comparisons the job's input cites under `evidence`. The edge keeps a cited ID only when it is the tenant's, with what it knows of it (a finding's kind, an eval run's passes, a comparison's verdict); an ID it does not know is left out and counted.<br />

An evidence read opens in the audit log (filtered to evidence reads of the last 30 days; choose an earlier period for an older one) and a comparison on its own page. An eval finding and an eval run have no page that opens one by its ID, so they are shown as IDs: `oz0 eval findings` and `oz0 eval runs` list them.

The changeset card on the Approvals page and `oz0 changes show` show it before anyone decides:

```text
Job job-0192...'s changeset: proposed
  Made by an agent:
    Job:       job-0192... (https://oz.example/t/acme/jobs/job-0192...)
    Agent:     improver of plugin-improver@0.1.0
    Model:     smart (anthropic/claude-sonnet-5-5)
    Changes:   plugin triage@1.4.0
    Evidence:  read:0192a6b4... (triage: findings, evals)
               comparison:cmp-3 (better)
```

The same record goes to the function that applies the changeset, beside the call's arguments where only the platform sets it. `oz0_github.push_changes` writes it into the pull request's body, as a section after the body (with links to the job and to each evidence read and comparison when the cluster names its console's address), and as the commit's Git trailers, the message's last paragraph, which `git interpret-trailers --parse` reads:

```text
Fix the triage agent's outcome check

Applied by Orchestrator Zero after review.

OZ-Job: job-0192...
OZ-Agent: improver
OZ-Plugin: plugin-improver@0.1.0
OZ-Model: smart (anthropic/claude-sonnet-5-5)
OZ-Evidence: read:0192a6b4... (triage: findings, evals)
OZ-Evidence: comparison:cmp-3 (better)
```

A person's own call of `push_changes` carries none, and an argument the model writes never adds one. A version installed from such a commit keeps the record: see [where a version came from](https://docs.oz0.ai/build/plugins/install#where-a-version-came-from). A job from before this record, or an edge that cannot read the evidence, gives a record without evidence, which says why.

### An improver's changesets

The [Plugin improver](https://docs.oz0.ai/build/plugins/verified/plugin-improver) (`ai.oz0/plugin-improver`) is an agent that proposes reviewed, evidence-backed changes to your plugins through exactly this path: its coding agent declares `changes: {review: required, apply: oz0_github.push_changes, base: archive, target: target, evidence: evidence}`, so every changeset it makes waits for a person, names the plugin it changes, and carries the evidence its job cited.

Before anyone is asked, a verify script pinned from its definition checks that each plugin the change wrote to still loads as a plugin, a judge holds its answer to the default rubric, the protected paths and the target's change budget apply as for any agent, and its card shows where the change came from.

Its changeset is also checked as a [candidate](https://docs.oz0.ai/build/plugins/develop-and-test/candidate-checks#a-changesets-candidate-check) of its target before anyone is asked: built and linted as an install, and its changed agents compared on their evals against the installed version; what that found is shown beside the approval as evidence, never as an approval.

Its pull requests go on branches named `oz0/improver-<...>`. Reviewing one is reviewing any changeset: read the diff, the record and the agent's answer, and approve exactly that patch or reject it.


## Sitemap

See the full [sitemap](https://docs.oz0.ai/sitemap.md) for all pages.
