Reviewed changes

Review assistant and job proposals

Review assistant and job proposals for tenant admins and governance reviewers.

For: tenant admins and governance reviewers.

Assistant plans

The setup assistant proposes changes as one change request of kind assistant_plan, requested by the person who asked it. It holds the admin API calls the plan makes, in order, each with what it changes before and after and the permission it needs:

Terminal
$ oz0 change show cr-0af29805-61f3
Change request cr-0af29805-61f3: assistant_plan of job-bb11a8da-..., pending
  requested by user:ada 1s ago
  proposed by the setup assistant (oz0-setup-assistant, plugin setup-assistant) in job job-bb11a8da-... for user:ada
  A shadow rule: never email outside example.com
Calls, applied in order as the approver when approved:
  1. SetToolPolicy (needs governance.write)
     - rule-8e8ff424bb0e: (none)
     + rule-8e8ff424bb0e: shadow refuse mail.send when !args.to.all(r, r.endsWith('@example.com')) ("never email outside example.com")
  2. StartReplay (needs governance.write and content.read)
     + replay the tenant's jobs of 7d with rule-8e8ff424bb0e turned on, and link the report here
Delta sha256:4446... (review-widening confirm)

Read and approve a plan

Read a plan as you would read the commands it stands for. The approval names its digest, as for every change request. Approving it applies each call through the admin API as the approver, with the approver's own permissions, checks and audit entries (via: assistant, and the assistant's job): a call the approver may not make is refused and reported, the calls after it still run, and the plan ends applied when every call applied and failed otherwise, each call's outcome kept.

A call that loosens a governance setting under four eyes opens its own change request, which waits for another admin. A tool policy call is refused when the policy changed after the plan was made, and a new plan from the same job supersedes the one that waited. Under review_widening: confirm the person who asked may approve their own plan; under second_admin another admin does.

Who can read and approve its contents

A plan is what a job wrote, and a job's words can repeat what it read, so it keeps that job's content decision. A reviewer the job's content is closed to (no content permission, a wall, a turn of someone else's chat) sees the request's kind, job, state and the calls' names and permissions, and no title, summary, request or digest, and may reject it but not approve it.

A proposed update's note is kept the same way. The audit log follows the same decision: the lines, the digest and why applying failed are in the private part of an entry about such a request, and a chat's words stay out of the log, in the request.

Who can propose a setup plan

A setup plan is always a person's: a job that a key, an app, a routine or an outside event started cannot file one, since the plan is read and applied as the person who started it, and such a job has nobody to read as, nobody to count for four eyes, and an outside event's text could steer its model into admin calls under a person's name.

Change requests a job files

One kind of change request may come from a job that no person started, such as a routine's fire on a merged pull request: plugin_update, an update of an installed plugin. A job asks for it with the platform built-in oz.propose_update(plugin, canary?, soak?, shadow_runs?, note?), from an agent or a flow step, and it is the only kind a job may file: never a governance change, an erasure, a credential, egress, a plugin's configuration or a setup plan.

Eligible plugins

The target must be installed from Git, and the job's plugin must hold a results grant for it. The target cannot be the job's own plugin. A person must use oz0 plugin update to update a plugin installed from a hub listing, an upload or an image.

What the platform records

The platform writes its lines from its own records: how the plugin is installed (its source, ref, folder, node selector, repository secret and grants, kept as oz0 plugin update keeps them), the commit its ref resolves to when the request is filed, and the rollout (canary nodes, soak, shadow runs), bounded as the CLI's flags. The job's own words are a note of at most 500 characters, shown as the job's. A ref that still resolves to the installed commit files nothing.

Who requested the update

The request names the job (job:<root>), its plugin, who started it (a person, a key, an app, a routine or an outside event) and the trigger, never a person in the job's name. It records the job's taint, and the request says when an outside event started the job and that its note is that job's words.

Who may approve

Only a person holding plugins.install, the permission of the update it runs, may approve. An API key, an app, a policy or a job cannot approve. Under second_admin or four eyes, a person whose own job filed it does not approve it: the job's starter, the person who created the API key it was started with (even after the key is revoked), the person who started it through an app, and the people a chat turn started it for. When who started it cannot be read, nobody approves it until it can.

What approval runs

Approval runs the update as the approver, through the update path: the update's lint, the plugin's evals on a test node, the custody check (an update from new hands files a plugin_custody request of its own), and the staged rollout behind the quality gates. It updates to exactly the commit the request shows; when the ref resolves to another one by then, or how the plugin is installed changed, nothing is updated and the request says so.

What the update widens is the approver's own update's: refused unless granted, or a change request of its own in the approver's name, which under second_admin another admin decides.

Queue limits and expiry

One request may wait per plugin; a new one replaces it. At most 20 may wait in a tenant. A request that waits 14 days is superseded with the note expired.

Inspect a request and its audit entries

Terminal
$ oz0 change show cr-0193a3c2-11
Change request cr-0193a3c2-11: plugin_update of notes, pending
  requested by job:improver-after-merge-7c1e 2m ago; from https://github.com/acme/notes.git main at 8e7d6c5b4a3f...
  + update notes from https://github.com/acme/notes.git, ref main, to commit 8e7d6c5b4a3f
  + keeps its grants, node selector pool=improver
  + rollout: 1 canary node(s) first, a soak of 10m0s a step, up to 10 shadow runs, behind the quality gates
  + filed by job improver-after-merge-7c1e of plugin plugin-improver, started by routine plugin-improver-merged (trigger closed, event (github: pull_request))
  + an outside event started that job: read its note as that event's words, not the platform's
  + the job's note, in its own words: Pull request #7 of acme/notes was merged into main, now at 8e7d6c5b4a3f.

The audit log has change.plugin_update.requested (by the job, with its trigger, the plugin and the commit), .approved, .applied and .expired. The Plugin improver files one after each merge of its pull requests.

Copyright © 2026