Improve

Evidence for agents

Let an agent read another plugin's job outcomes and install details under a grant, with the granting person's permissions checked at every read.

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:

KindWhatNeeds, of the granting person
findingsThe target's findings, open and resolved: kind, agent, state, messagefindings.read
failure_classesNot available in this release
failure_kindsHow 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 ownfindings.read
evalsFailing cases of the target's eval runs: eval, case, run, version, verdict; with content, the case's input and expected outputusage.read
kept_casesCases kept from the target's jobs: eval, case, job, label, failures and runs; with content, the input and expected outputfindings.read
refused_approvalsApprovals people refused on the target's agents' jobs: job, agent, version, approval, when; with content, the reason the person gavejobs.read
errorsError 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 themfindings.read
costCost per run per agent and version, and per team the calls were metered tousage.read
scorecardsPer agent and version: runs, successes, success rate, cost, cost per success; and the figures of people's decisions on its approvalsusage.read
installedWhere the target is installed from and whose it is: see belowtenant.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:

FieldWhat
version, commitThe installed version and the commit (or digest) it is at
source_kind, sourcegit 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, refIts folder in the repository and the ref it follows (empty: the highest release tag)
secretThe name of the tenant secret that fetches a private source, never its value
selectorThe node selector it runs under
custodyWhose 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_atWho installed it and when
signed_byThe 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):

oz0-plugin.yaml
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 as 7d or 72h; 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: the next_page_token of 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.

Copyright © 2026