Evidence for agents
For: Plugin authors giving agents access to evidence.
Grant a plugin reads: [results] when its agents need evidence to propose changes. The built-in oz.evidence tool reads results from the plugins named in the grant, including failures, refused approvals and cost. It writes nothing.
Every read checks the granting person's current permissions. A grant cannot give the agent access that person no longer holds.
What it reads
For one target plugin (and optionally one of its agents or flows), over the last 30 days by default and at most 90:
| Kind | What | Needs, of the granting person |
|---|---|---|
findings | The target's findings, open and resolved: kind, agent, state, message | findings.read |
failure_classes | Not available in this release | |
failure_kinds | How the target's agents' jobs failed: per agent, each failure kind its jobs ended with, how many, and whether the agent declares the kind or it is one of the platform's own | findings.read |
evals | Failing cases of the target's eval runs: eval, case, run, version, verdict; with content, the case's input and expected output | usage.read |
kept_cases | Cases kept from the target's jobs: eval, case, job, label, failures and runs; with content, the input and expected output | findings.read |
refused_approvals | Approvals people refused on the target's agents' jobs: job, agent, version, approval, when; with content, the reason the person gave | jobs.read |
errors | Error groups of the target's agents: fingerprint, step, version, count, jobs, first and last seen, and the failure's kind in a fixed sentence; on a group an older edge kept with its words, those words and an example while the granting person may read them | findings.read |
cost | Cost per run per agent and version, and per team the calls were metered to | usage.read |
scorecards | Per agent and version: runs, successes, success rate, cost, cost per success; and the figures of people's decisions on its approvals | usage.read |
installed | Where the target is installed from and whose it is: see below | tenant.read |
Content is what jobs read and wrote: eval inputs and expected outputs, the reasons people gave, error examples. It is in the answer only while the granting person holds content.read. A kind whose permission they lack is left out with the permission it needs. A group's words name no job, so a person a wall applies to gets the group's facts without them, whatever they hold.
Where a target is installed from
The installed kind answers, for the target, one row of the platform's own record of its install:
| Field | What |
|---|---|
version, commit | The installed version and the commit (or digest) it is at |
source_kind, source | git and its URL with any user, password or token taken out; hub and its listing (hub:<publisher>/<name>); oci and its reference; bundle and "uploaded bundle"; or workspace |
path, ref | Its folder in the repository and the ref it follows (empty: the highest release tag) |
secret | The name of the tenant secret that fetches a private source, never its value |
selector | The node selector it runs under |
custody | Whose hands the tenant accepted it from: the hub index and listing, the custody epoch and publisher, the forge, repository ID and owner |
installed_by, installed_at, updated_at | Who installed it and when |
signed_by | The hub index whose signed listing the version came from, and who trusted its keys; empty for Git and uploads |
An agent that proposes changes to a plugin reads it before it touches anything: the Plugin improver refuses a target installed from another repository or folder than its setting names, from another publisher's listing or from a repository in other hands. A target outside the grant is refused as for every kind, and each read is audited as every read.
Who and what is left out
- Another tenant's data, never. The edge takes the tenant from the node's certificate.
- A data subject's jobs, never. Every kind leaves out the jobs started for a data subject and the cases kept from them.
- No person's figures. Cost is per agent, version and team, never per person, and no answer names who decided an approval.
- Small teams. With works-council mode on (the default), a team's cost shows only when the team has at least the mode's threshold of people (5 by default), and the figures of people's decisions only when at least that many people decided; otherwise they say "too few to show".
- Secrets and settings' values, never.
The grant
A plugin declares the kind in its manifest (reads):
reads: [results]
A person who may read content (content.read) and install plugins grants it for the target plugins they name. A key never grants it: the reads are answered with a person's permissions.
oz0 plugin install https://github.com/acme/plugin-improver --accept-reads results --reads-target triage --reads-target billing
The install screen, in the console and on the CLI, asks which plugins when you leave the targets out; none, and the plugin reads nothing. Later:
oz0 plugin reads grant plugin-improver results --target desk --reason "the desk's refused approvals"
oz0 plugin reads revoke plugin-improver results --target billing
A target added widens what the plugin reads, so it takes a reason and, when the tenant has four eyes on, waits for another admin; whoever asked is the granting person. Taking a target back applies at once. Both are in the audit log (plugin.reads.granted, plugin.reads.revoked, with the target). Revoking the kind (oz0 plugin revoke-reads plugin-improver results) or removing the plugin drops every target.
A target ends with the plugin it names too: removing triage, or installing a plugin named triage from another origin, takes it out of every results grant (audited as plugin.reads.revoked, with the reason), so a plugin installed later under that name is read only once a person grants it again.
oz0 plugin dependents triage lists the plugins that read its results; they do not block its removal. An edge never answers a target from its copy while management is away: the read waits for management.
The granting person's permissions are read again at every read. An improver's jobs are usually started by routines and events, so the reads are answered as the person who granted the target, not as whoever started the job. If that person loses content.read or leaves the tenant, or their account is disabled, the reads stop; if they lose findings.read or usage.read, the kinds that need it are left out.
Either way an evidence_grant_lapsed finding names the plugin and the target. A person who may read content grants it again with oz0 plugin reads grant <plugin> results --target <target> --reason "...": their permissions answer the reads from then on, which is a loosening (a reason, and four eyes where the tenant has them on), and the finding is resolved.
The finding is also resolved when a read finds the person who granted it holding every permission again.
The tool
oz.evidence(plugin, agent?, since?, kinds?, limit?, page_token?)
plugin: a target the plugin was granted.agent: one of the target's agents or flows.since: how far back, such as7dor72h; 30 days by default, at most 90.kinds: any of the kinds above; all by default.limit: rows of each kind, 1 to 50 (20 by default).page_token: thenext_page_tokenof an earlier answer, with the same arguments, in the same job and within a day of the first page. The edge signs each token, so a token the model changes or makes itself is refused.
An answer holds at most 256 KB and 50 rows of each kind; what did not fit comes with next_page_token, and a kind whose rows go on says more. Pages go at most 1,000 rows deep.
{
"tenant": "acme", "plugin": "triage", "since": "2026-10-13T09:00:00Z", "until": "2026-10-20T09:00:00Z",
"content": true,
"kinds": {
"findings": {"rows": [{"id": "…", "kind": "eval_regression", "agent": "triager", "state": "open", "message": "…"}]},
"refused_approvals": {"rows": [{"job": "…", "agent": "triager", "approval": "github.merge", "reason": "the label is wrong"}]},
"cost": {"rows": [{"agent": "triager", "version": "1.4.0", "runs": 120, "cost_usd": 3.1, "cost_per_run_usd": 0.026}],
"teams": [{"team": "support", "jobs": 80, "cost_usd": 2.0}, {"team": "legal", "hidden": "too few to show"}]},
"failure_classes": {"rows": [], "hidden": "not available in this release"}
},
"next_page_token": "…",
"notes": ["a data subject's jobs are left out of every kind"]
}
Every agent of a plugin that declares reads: [results] is offered the tool (a routine's fire only when its routine lists it); an ACP harness session gets it from its MCP server. The read runs as a step on the job's home node, which asks the edge with the step's own call binding, so no tool process or session can read in a job's name. What the answer holds of other jobs is untrusted data: an answer with content taints the job (taint).
Every read is audited, as run.evidence_read with the job, its agent, the target, the kinds and how many rows of each came back, never the content. An agent's proposed change can cite the reads it was based on.
Its events
The same grant brings the built-in event source results: routines and flows of the plugin fire on a new finding of a target (finding.opened) and on a model change that reaches a target's agents (model.changed). See Event sources.
Where a fix belongs
When an agent fails a job, keep the failure as an eval case, put the fix in the cheapest part that fixes it, and run the eval again. Seven parts, cheapest first, each with an example from the plugins OZ ships.
Govern agents
Set rules for agent work, review changes and keep the evidence needed to explain decisions.